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

import SignalServiceKit
import SignalUI

public protocol ForwardMessageDelegate: AnyObject {
    func forwardMessageFlowDidComplete(items: [ForwardMessageItem],
                                       recipientThreads: [TSThread])
    func forwardMessageFlowDidCancel()
}

class ForwardMessageViewController: InteractiveSheetViewController {

    private let pickerVC: ForwardPickerViewController
    private let forwardNavigationViewController = ForwardNavigationViewController()

    override var interactiveScrollViews: [UIScrollView] { [ pickerVC.tableView ] }

    public weak var forwardMessageDelegate: ForwardMessageDelegate?

    public typealias Item = ForwardMessageItem
    fileprivate typealias Content = ForwardMessageContent

    fileprivate var content: Content

    fileprivate var textMessage: String?

    private let selection = ConversationPickerSelection()
    var selectedConversations: [ConversationItem] { selection.conversations }

    override var sheetBackgroundColor: UIColor {
        ForwardPickerViewController.tableBackgroundColor(isUsingPresentedStyle: true)
    }

    private init(content: Content) {
        self.content = content
        self.pickerVC = ForwardPickerViewController(selection: selection)

        super.init()

        if self.content.canSendToStories {
            if self.content.canSendToNonStories {
                self.pickerVC.sectionOptions.insert(.stories)
            } else {
                self.pickerVC.sectionOptions = .storiesOnly
            }
        } else {
            self.pickerVC.shouldHideRecentConversationsTitle = true
        }

        selectRecipientsStep()

        minimizedHeight = 576
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public class func present(forItemViewModels itemViewModels: [CVItemViewModelImpl],
                              from fromViewController: UIViewController,
                              delegate: ForwardMessageDelegate) {
        do {
            let content: Content = try Self.databaseStorage.read { transaction in
                try Content.build(itemViewModels: itemViewModels, transaction: transaction)
            }
            present(content: content, from: fromViewController, delegate: delegate)
        } catch {
            ForwardMessageViewController.showAlertForForwardError(error: error,
                                                                        forwardedInteractionCount: itemViewModels.count)
        }
    }

    public class func present(forSelectionItems selectionItems: [CVSelectionItem],
                              from fromViewController: UIViewController,
                              delegate: ForwardMessageDelegate) {
        do {
            let content: Content = try Self.databaseStorage.read { transaction in
                try Content.build(selectionItems: selectionItems, transaction: transaction)
            }
            present(content: content, from: fromViewController, delegate: delegate)
        } catch {
            ForwardMessageViewController.showAlertForForwardError(error: error,
                                                                        forwardedInteractionCount: selectionItems.count)
        }
    }

    public class func present(
        forAttachmentStreams attachmentStreams: [ReferencedTSResourceStream],
        fromMessage message: TSMessage,
        from fromViewController: UIViewController,
        delegate: ForwardMessageDelegate
    ) {
        do {
            let builder = Item.Builder(interaction: message)

            builder.attachments = try attachmentStreams.map { attachmentStream in
                try DependenciesBridge.shared.tsResourceCloner.cloneAsSignalAttachment(
                    attachment: attachmentStream
                )
            }

            let item: Item = builder.build()

            present(
                content: .single(item: item),
                from: fromViewController,
                delegate: delegate
            )
        } catch let error {
            ForwardMessageViewController.showAlertForForwardError(
                error: error,
                forwardedInteractionCount: 1
            )
        }
    }

    public class func present(
        forStoryMessage storyMessage: StoryMessage,
        from fromViewController: UIViewController,
        delegate: ForwardMessageDelegate
    ) {
        let builder = Item.Builder()
        switch storyMessage.attachment {
        case .file, .foreignReferenceAttachment:
            let attachment: ReferencedTSResourceStream? = databaseStorage.read { tx in
                guard
                    let reference = DependenciesBridge.shared.tsResourceStore.mediaAttachment(for: storyMessage, tx: tx.asV2Read),
                    let attachmentStream = reference.fetch(tx: tx)?.asResourceStream()
                else {
                    return nil
                }
                return .init(reference: reference, attachmentStream: attachmentStream)
            }
            do {
                guard let attachment else {
                    throw OWSAssertionError("Missing attachment stream for forwarded story message")
                }
                let signalAttachment = try DependenciesBridge.shared.tsResourceCloner.cloneAsSignalAttachment(
                    attachment: attachment
                )
                builder.attachments = [signalAttachment]
            } catch let error {
                ForwardMessageViewController.showAlertForForwardError(
                    error: error,
                    forwardedInteractionCount: 1
                )
                return
            }
        case .text(let textAttachment):
            builder.textAttachment = textAttachment
        }
        present(content: .single(item: builder.build()), from: fromViewController, delegate: delegate)
    }

    private class func present(content: Content,
                               from fromViewController: UIViewController,
                               delegate: ForwardMessageDelegate) {

        let sheet = ForwardMessageViewController(content: content)
        sheet.forwardMessageDelegate = delegate
        fromViewController.present(sheet, animated: true) {
            UIApplication.shared.hideKeyboard()
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        ensureBottomFooterVisibility()
    }

    private func selectRecipientsStep() {
        pickerVC.forwardMessageViewController = self
        pickerVC.shouldShowSearchBar = false
        pickerVC.shouldHideSearchBarIfCancelled = true
        pickerVC.pickerDelegate = self
        pickerVC.shouldBatchUpdateIdentityKeys = true

        forwardNavigationViewController.forwardMessageViewController = self
        forwardNavigationViewController.viewControllers = [ pickerVC ]
        self.addChild(forwardNavigationViewController)

        let navView = forwardNavigationViewController.view!
        self.contentView.addSubview(navView)
        navView.autoPinEdgesToSuperviewEdges()
    }

    fileprivate func selectSearchBar() {
        AssertIsOnMainThread()

        pickerVC.selectSearchBar()
        ensureHeaderVisibility()
    }

    fileprivate func ensureHeaderVisibility() {
        AssertIsOnMainThread()

        forwardNavigationViewController.setNavigationBarHidden(pickerVC.isSearchBarActive, animated: false)
        if pickerVC.isSearchBarActive {
            maximizeHeight()
        }
    }

    fileprivate func ensureBottomFooterVisibility() {
        AssertIsOnMainThread()

        if selectedConversations.allSatisfy({ $0.outgoingMessageType == .storyMessage }) {
            pickerVC.approvalTextMode = .none
        } else {
            let placeholderText = OWSLocalizedString(
                "FORWARD_MESSAGE_TEXT_PLACEHOLDER",
                comment: "Indicates that the user can add a text message to forwarded messages."
            )
            pickerVC.approvalTextMode = .active(placeholderText: placeholderText)
        }

        pickerVC.shouldHideBottomFooter = selectedConversations.isEmpty
    }

    public override func willDismissInteractively() {
        AssertIsOnMainThread()

        forwardMessageDelegate?.forwardMessageFlowDidCancel()
    }
}

// MARK: - Sending

extension ForwardMessageViewController {

    private static let keyValueStore = SDSKeyValueStore(collection: "ForwardMessageViewController")
    private static let hasForwardedKey = "hasForwardedKey"

    private var hasForwardedWithSneakyTransaction: Bool {
        databaseStorage.read { transaction in
            Self.keyValueStore.getBool(Self.hasForwardedKey, defaultValue: false, transaction: transaction)
        }
    }
    private static func markHasForwardedWithSneakyTransaction() {
        databaseStorage.write { transaction in
            Self.keyValueStore.setBool(true, key: Self.hasForwardedKey, transaction: transaction)
        }
    }

    func sendStep() {
        if hasForwardedWithSneakyTransaction {
            tryToSend()
        } else {
            showFirstForwardAlert()
        }
    }

    private func showFirstForwardAlert() {
        let actionSheet = ActionSheetController(
            title: OWSLocalizedString("FORWARD_MESSAGE_FIRST_FORWARD_TITLE",
                                     comment: "Title for alert with information about forwarding messages."),
            message: OWSLocalizedString("FORWARD_MESSAGE_FIRST_FORWARD_MESSAGE",
                                     comment: "Message for alert with information about forwarding messages.")
            )

        let format = OWSLocalizedString("FORWARD_MESSAGE_FIRST_FORWARD_PROCEED_%d", tableName: "PluralAware",
                                       comment: "Format for label for button to proceed with forwarding multiple messages. Embeds: {{ the number of forwarded messages. }}")
        let actionTitle = String.localizedStringWithFormat(format, content.allItems.count)
        actionSheet.addAction(ActionSheetAction(title: actionTitle) { [weak self] _ in
            Self.markHasForwardedWithSneakyTransaction()

            self?.tryToSend()
        })
        actionSheet.addAction(OWSActionSheets.cancelAction)
        presentActionSheet(actionSheet)
    }

    private func tryToSend() {
        AssertIsOnMainThread()

        do {
            try tryToSendThrows()
        } catch {
            owsFailDebug("Error: \(error)")

            self.forwardMessageDelegate?.forwardMessageFlowDidCancel()
        }
    }

    private func tryToSendThrows() throws {
        let content = self.content
        let textMessage = self.textMessage?.strippedOrNil

        let recipientConversations = self.selectedConversations
        firstly(on: DispatchQueue.global()) {
            self.outgoingMessageRecipientThreads(for: recipientConversations)
        }.then(on: DispatchQueue.main) { (outgoingMessageRecipientThreads: [TSThread]) -> Promise<Void> in
            try Self.databaseStorage.write { transaction in
                for recipientThread in outgoingMessageRecipientThreads {
                    // We're sending a message to this thread, approve any pending message request
                    ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequest(
                        recipientThread,
                        setDefaultTimerIfNecessary: true,
                        tx: transaction
                    )
                }

                func hasRenderableContent(interaction: TSInteraction, tx: SDSAnyReadTransaction) -> Bool {
                    guard let message = interaction as? TSMessage else {
                        return false
                    }
                    return message.hasRenderableContent(tx: tx)
                }

                // Make sure the message and its content haven't been deleted (view-once
                // messages, remove delete, disappearing messages, manual deletion, etc.).
                for item in content.allItems where item.interaction != nil {
                    guard
                        let interactionId = item.interaction?.uniqueId,
                        let latestInteraction = TSInteraction.anyFetch(
                            uniqueId: interactionId,
                            transaction: transaction
                        ),
                        hasRenderableContent(interaction: latestInteraction, tx: transaction)
                    else {
                        throw ForwardError.missingInteraction
                    }
                }
            }

            // TODO: Ideally we would enqueue all with a single write transaction.
            return firstly { () -> Promise<Void> in
                // Maintain order of interactions.
                let sortedItems = content.allItems.sorted { lhs, rhs in
                    lhs.interaction?.timestamp ?? 0 < rhs.interaction?.timestamp ?? 0
                }
                // _Enqueue_ each item serially.
                // Each item waits on the previous' enqueue promise, then sets its own
                // enqueue promise as the previous promise var.
                var prevEnqueuePromise = Promise<Void>.value(())
                for item in sortedItems {
                    prevEnqueuePromise = prevEnqueuePromise.then(on: DispatchQueue.main) {
                        return self.send(item: item, toOutgoingMessageRecipientThreads: outgoingMessageRecipientThreads)
                    }
                }
                return prevEnqueuePromise.then(on: DispatchQueue.main) { _ -> Promise<Void> in
                    // The user may have added an additional text message to the forward.
                    // It should be sent last.
                    if let textMessage = textMessage {
                        let messageBody = MessageBody(text: textMessage, ranges: .empty)
                        return self.send(toRecipientThreads: outgoingMessageRecipientThreads) { recipientThread in
                            self.send(body: messageBody, recipientThread: recipientThread)
                        }
                    } else {
                        return Promise.value(())
                    }
                }
            }.map(on: DispatchQueue.main) {
                self.forwardMessageDelegate?.forwardMessageFlowDidComplete(
                    items: content.allItems,
                    recipientThreads: outgoingMessageRecipientThreads
                )
            }
        }.catch(on: DispatchQueue.main) { error in
            owsFailDebug("Error: \(error)")

            Self.showAlertForForwardError(error: error, forwardedInteractionCount: content.allItems.count)
        }
    }

    private func send(item: Item, toOutgoingMessageRecipientThreads outgoingMessageRecipientThreads: [TSThread]) -> Promise<Void> {
        AssertIsOnMainThread()

        if let stickerMetadata = item.stickerMetadata {
            let stickerInfo = stickerMetadata.stickerInfo
            if StickerManager.isStickerInstalled(stickerInfo: stickerInfo) {
                return send(toRecipientThreads: outgoingMessageRecipientThreads) { recipientThread in
                    self.send(installedSticker: stickerInfo, thread: recipientThread)
                }
            } else {
                guard let stickerAttachment = item.stickerAttachment else {
                    return Promise(error: OWSAssertionError("Missing stickerAttachment."))
                }
                do {
                    let stickerData = try stickerAttachment.decryptedRawData()
                    return send(toRecipientThreads: outgoingMessageRecipientThreads) { recipientThread in
                        self.send(uninstalledSticker: stickerMetadata,
                                  stickerData: stickerData,
                                  thread: recipientThread)
                    }
                } catch {
                    return Promise(error: error)
                }
            }
        } else if let contactShare = item.contactShare {
            return send(toRecipientThreads: outgoingMessageRecipientThreads) { recipientThread in
                return self.send(contactShare: contactShare.copyForResending(), thread: recipientThread)
            }
        } else if let attachments = item.attachments,
                  !attachments.isEmpty {
            // TODO: What about link previews in this case?
            let conversations = selectedConversations
            return TSResourceMultisend.sendApprovedMedia(
                conversations: conversations,
                approvalMessageBody: item.messageBody,
                approvedAttachments: attachments
            ).enqueuedPromise.asVoid()
        } else if let textAttachment = item.textAttachment {
            // TODO: we want to reuse the uploaded link preview image attachment instead of re-uploading
            // if the original was sent recently (if not the image could be stale)
            return TSResourceMultisend.sendTextAttachment(
                textAttachment.asUnsentAttachment(), to: selectedConversations
            ).enqueuedPromise.asVoid()
        } else if let messageBody = item.messageBody {
            let linkPreviewDraft = item.linkPreviewDraft
            let nonStorySendPromise = send(toRecipientThreads: outgoingMessageRecipientThreads) { recipientThread in
                self.send(
                    body: messageBody,
                    linkPreviewDraft: linkPreviewDraft,
                    recipientThread: recipientThread
                )
            }

            // Send the text message to any selected story recipients
            // as a text story with default styling.
            let storyConversations = selectedConversations.filter { $0.outgoingMessageType == .storyMessage }
            let storySendResult = StorySharing.sendTextStory(with: messageBody, linkPreviewDraft: linkPreviewDraft, to: storyConversations)
            let storySendEnqueuedPromise = storySendResult?.enqueuedPromise.asVoid() ?? .value(())
            return Promise<Void>.when(fulfilled: [nonStorySendPromise, storySendEnqueuedPromise])
        } else {
            return Promise(error: ForwardError.invalidInteraction)
        }
    }

    fileprivate func send(body: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft? = nil, recipientThread: TSThread) -> Promise<Void> {
        let body = databaseStorage.read { transaction in
            return body.forForwarding(to: recipientThread, transaction: transaction.unwrapGrdbRead).asMessageBodyForForwarding()
        }
        ThreadUtil.enqueueMessage(
            body: body,
            thread: recipientThread,
            linkPreviewDraft: linkPreviewDraft
        )
        return Promise.value(())
    }

    fileprivate func send(contactShare: ContactShareDraft, thread: TSThread) -> Promise<Void> {
        ThreadUtil.enqueueMessage(withContactShare: contactShare, thread: thread)
        return Promise.value(())
    }

    fileprivate func send(body: MessageBody, attachment: SignalAttachment, thread: TSThread) -> Promise<Void> {
        let body = databaseStorage.read { transaction in
            return body.forForwarding(to: thread, transaction: transaction.unwrapGrdbRead).asMessageBodyForForwarding()
        }
        ThreadUtil.enqueueMessage(body: body,
                                  mediaAttachments: [attachment],
                                  thread: thread)
        return Promise.value(())
    }

    fileprivate func send(installedSticker stickerInfo: StickerInfo, thread: TSThread) -> Promise<Void> {
        ThreadUtil.enqueueMessage(withInstalledSticker: stickerInfo, thread: thread)
        return Promise.value(())
    }

    fileprivate func send(uninstalledSticker stickerMetadata: any StickerMetadata, stickerData: Data, thread: TSThread) -> Promise<Void> {
        ThreadUtil.enqueueMessage(withUninstalledSticker: stickerMetadata, stickerData: stickerData, thread: thread)
        return Promise.value(())
    }

    fileprivate func send(toRecipientThreads recipientThreads: [TSThread],
                          enqueueBlock: @escaping (TSThread) -> Promise<Void>) -> Promise<Void> {
        AssertIsOnMainThread()

        return Promise.when(fulfilled: recipientThreads.map { thread in enqueueBlock(thread) }).asVoid()
    }

    fileprivate func outgoingMessageRecipientThreads(for conversationItems: [ConversationItem]) -> Promise<[TSThread]> {
        firstly(on: DispatchQueue.global()) {
            guard conversationItems.count > 0 else {
                throw OWSAssertionError("No recipients.")
            }

            return try self.databaseStorage.write { transaction in
                try conversationItems.lazy.filter { $0.outgoingMessageType == .message }.map {
                    guard let thread = $0.getOrCreateThread(transaction: transaction) else {
                        throw ForwardError.missingThread
                    }
                    return thread
                }
            }
        }
    }
}

// MARK: -

extension ForwardMessageViewController: ConversationPickerDelegate {
    func conversationPickerSelectionDidChange(_ conversationPickerViewController: ConversationPickerViewController) {
        ensureBottomFooterVisibility()
    }

    func conversationPickerDidCompleteSelection(_ conversationPickerViewController: ConversationPickerViewController) {
        self.textMessage = conversationPickerViewController.textInput?.strippedOrNil

        sendStep()
    }

    func conversationPickerCanCancel(_ conversationPickerViewController: ConversationPickerViewController) -> Bool {
        false
    }

    func conversationPickerDidCancel(_ conversationPickerViewController: ConversationPickerViewController) {
        forwardMessageDelegate?.forwardMessageFlowDidCancel()
    }

    func approvalMode(_ conversationPickerViewController: ConversationPickerViewController) -> ApprovalMode {
        .send
    }

    func conversationPickerDidBeginEditingText() {
        AssertIsOnMainThread()

        maximizeHeight()
    }

    func conversationPickerSearchBarActiveDidChange(_ conversationPickerViewController: ConversationPickerViewController) {
        ensureHeaderVisibility()
    }
}

// MARK: -

extension TSAttachmentStream {

}

// MARK: -

extension ForwardMessageViewController {
    public static func finalizeForward(items: [Item],
                                       recipientThreads: [TSThread],
                                       fromViewController: UIViewController) {
        let toast: String
        if items.count > 1 {
            toast = OWSLocalizedString("FORWARD_MESSAGE_MESSAGES_SENT_N",
                                      comment: "Indicates that multiple messages were forwarded.")
        } else {
            toast = OWSLocalizedString("FORWARD_MESSAGE_MESSAGES_SENT_1",
                                      comment: "Indicates that a single message was forwarded.")
        }
        fromViewController.presentToast(text: toast)
    }
}

// MARK: -

public enum ForwardError: Error {
    case missingInteraction
    case missingThread
    case invalidInteraction
}

// MARK: -

extension ForwardMessageViewController {

    public static func showAlertForForwardError(error: Error,
                                                forwardedInteractionCount: Int) {
        let genericErrorMessage = (forwardedInteractionCount > 1
                                    ? OWSLocalizedString("ERROR_COULD_NOT_FORWARD_MESSAGES_N",
                                                        comment: "Error indicating that messages could not be forwarded.")
                                    : OWSLocalizedString("ERROR_COULD_NOT_FORWARD_MESSAGES_1",
                                                        comment: "Error indicating that a message could not be forwarded."))

        guard let forwardError = error as? ForwardError else {
            owsFailDebug("Error: \(error).")
            OWSActionSheets.showErrorAlert(message: genericErrorMessage)
            return
        }

        switch forwardError {
        case .missingInteraction:
            let message = (forwardedInteractionCount > 1
                            ? OWSLocalizedString("ERROR_COULD_NOT_FORWARD_MESSAGES_MISSING_N",
                                                comment: "Error indicating that messages could not be forwarded.")
                            : OWSLocalizedString("ERROR_COULD_NOT_FORWARD_MESSAGES_MISSING_1",
                                                comment: "Error indicating that a message could not be forwarded."))
            OWSActionSheets.showErrorAlert(message: message)
        case .missingThread, .invalidInteraction:
            owsFailDebug("Error: \(error).")

            OWSActionSheets.showErrorAlert(message: genericErrorMessage)
        }
    }
}

// MARK: -

public struct ForwardMessageItem {
    fileprivate typealias Item = ForwardMessageItem

    let interaction: TSInteraction?

    let attachments: [SignalAttachment]?
    let contactShare: ContactShareViewModel?
    let messageBody: MessageBody?
    let linkPreviewDraft: OWSLinkPreviewDraft?
    let stickerMetadata: (any StickerMetadata)?
    let stickerAttachment: TSResourceStream?
    let textAttachment: TextAttachment?

    fileprivate class Builder {
        let interaction: TSInteraction?

        var attachments: [SignalAttachment]?
        var contactShare: ContactShareViewModel?
        var messageBody: MessageBody?
        var linkPreviewDraft: OWSLinkPreviewDraft?
        var stickerMetadata: (any StickerMetadata)?
        var stickerAttachment: TSResourceStream?
        var textAttachment: TextAttachment?

        init(interaction: TSInteraction? = nil) {
            self.interaction = interaction
        }

        func build() -> ForwardMessageItem {
            ForwardMessageItem(
                interaction: interaction,
                attachments: attachments,
                contactShare: contactShare,
                messageBody: messageBody,
                linkPreviewDraft: linkPreviewDraft,
                stickerMetadata: stickerMetadata,
                stickerAttachment: stickerAttachment,
                textAttachment: textAttachment
            )
        }
    }

    fileprivate var asBuilder: Builder {
        let builder = Builder(interaction: interaction)
        builder.attachments = attachments
        builder.contactShare = contactShare
        builder.messageBody = messageBody
        builder.linkPreviewDraft = linkPreviewDraft
        builder.stickerMetadata = stickerMetadata
        builder.stickerAttachment = stickerAttachment
        return builder
    }

    var isEmpty: Bool {
        if let attachments = attachments,
           !attachments.isEmpty {
            return false
        }
        if contactShare != nil ||
            messageBody != nil ||
            stickerMetadata != nil {
            return false
        }
        return true
    }

    fileprivate static func build(
        interaction: TSInteraction,
        componentState: CVComponentState,
        selectionType: CVSelectionType,
        transaction: SDSAnyReadTransaction
    ) throws -> Item {

        let builder = Builder(interaction: interaction)

        let shouldHaveText = (selectionType == .allContent ||
                                selectionType == .secondaryContent)
        let shouldHaveAttachments = (selectionType == .allContent ||
                                        selectionType == .primaryContent)

        guard shouldHaveText || shouldHaveAttachments else {
            throw ForwardError.invalidInteraction
        }

        if shouldHaveText,
           let displayableBodyText = componentState.displayableBodyText,
           !displayableBodyText.fullTextValue.isEmpty {

            switch displayableBodyText.fullTextValue {
            case .text(let text):
                builder.messageBody = MessageBody(text: text, ranges: .empty)
            case .attributedText(let text):
                builder.messageBody = MessageBody(text: text.string, ranges: .empty)
            case .messageBody(let hydratedBody):
                builder.messageBody = hydratedBody.asMessageBodyForForwarding(preservingAllMentions: true)
            }

            if let linkPreview = componentState.linkPreviewModel, let message = interaction as? TSMessage {
                builder.linkPreviewDraft = Self.tryToCloneLinkPreview(
                    linkPreview: linkPreview,
                    parentMessage: message,
                    transaction: transaction
                )
            }
        }

        if shouldHaveAttachments {
            if let oldContactShare = componentState.contactShareModel {
                builder.contactShare = oldContactShare.copyForRendering()
            }

            var attachmentStreams = [ReferencedTSResourceStream]()
            attachmentStreams.append(contentsOf: componentState.bodyMediaAttachmentStreams)
            if let attachmentStream = componentState.audioAttachmentStream {
                attachmentStreams.append(attachmentStream)
            }
            if let attachmentStream = componentState.genericAttachmentStream {
                attachmentStreams.append(attachmentStream)
            }

            if !attachmentStreams.isEmpty {
                builder.attachments = try attachmentStreams.map { attachmentStream in
                    try DependenciesBridge.shared.tsResourceCloner.cloneAsSignalAttachment(
                        attachment: attachmentStream
                    )
                }
            }

            if let stickerMetadata = componentState.stickerMetadata {
                builder.stickerMetadata = stickerMetadata

                if let stickerAttachment = componentState.stickerAttachment {
                    builder.stickerAttachment = stickerAttachment
                }
            }
        }

        let item = builder.build()
        guard !item.isEmpty else {
            throw ForwardError.invalidInteraction
        }
        return item
    }

    private static func tryToCloneLinkPreview(
        linkPreview: OWSLinkPreview,
        parentMessage: TSMessage,
        transaction: SDSAnyReadTransaction
    ) -> OWSLinkPreviewDraft? {
        guard let urlString = linkPreview.urlString,
              let url = URL(string: urlString) else {
            owsFailDebug("Missing or invalid urlString.")
            return nil
        }
        struct LinkPreviewImage {
            let imageData: Data
            let mimetype: String

            static func load(
                attachmentId: TSResourceId,
                transaction: SDSAnyReadTransaction
            ) -> LinkPreviewImage? {
                guard
                    let attachment = DependenciesBridge.shared.tsResourceStore
                        .fetch(attachmentId, tx: transaction.asV2Read)?
                        .asResourceStream()
                else {
                    owsFailDebug("Missing attachment.")
                    return nil
                }
                guard let mimeType = attachment.mimeType.nilIfEmpty else {
                    owsFailDebug("Missing mimeType.")
                    return nil
                }
                do {
                    let imageData = try attachment.decryptedRawData()
                    return LinkPreviewImage(imageData: imageData, mimetype: mimeType)
                } catch {
                    owsFailDebug("Error: \(error).")
                    return nil
                }
            }
        }
        var linkPreviewImage: LinkPreviewImage?
        if
            let imageAttachmentId = DependenciesBridge.shared.tsResourceStore.linkPreviewAttachment(
                for: parentMessage,
                tx: transaction.asV2Read
            )?.resourceId,
            let image = LinkPreviewImage.load(
                attachmentId: imageAttachmentId,
                transaction: transaction
            )
        {
            linkPreviewImage = image
        }
        let draft = OWSLinkPreviewDraft(url: url,
                                        title: linkPreview.title,
                                        imageData: linkPreviewImage?.imageData,
                                        imageMimeType: linkPreviewImage?.mimetype)
        draft.previewDescription = linkPreview.previewDescription
        draft.date = linkPreview.date
        return draft
    }
}

// MARK: -

private enum ForwardMessageContent {
    fileprivate typealias Item = ForwardMessageItem

    case single(item: Item)
    case multiple(items: [Item])

    var allItems: [Item] {
        switch self {
        case .single(let item):
            return [item]
        case .multiple(let items):
            return items
        }
    }

    var canSendToStories: Bool {
        allItems.allSatisfy { item in
            if let attachments = item.attachments {
                return attachments.allSatisfy({ $0.isValidImage || $0.isValidVideo })
            } else if item.textAttachment != nil {
                return true
            } else if item.messageBody != nil {
                return true
            } else {
                return false
            }
        }
    }

    var canSendToNonStories: Bool {
        allItems.allSatisfy { $0.textAttachment == nil }
    }

    private static func build(items: [Item]) -> ForwardMessageContent {
        if items.count == 1, let item = items.first {
            return .single(item: item)
        } else {
            return .multiple(items: items)
        }
    }

    fileprivate static func build(
        itemViewModels: [CVItemViewModelImpl],
        transaction: SDSAnyReadTransaction
    ) throws -> ForwardMessageContent {
        let items: [Item] = try itemViewModels.map { itemViewModel in
            try Item.build(interaction: itemViewModel.interaction,
                           componentState: itemViewModel.renderItem.componentState,
                           selectionType: .allContent,
                           transaction: transaction)
        }
        return build(items: items)
    }

    fileprivate static func build(
        selectionItems: [CVSelectionItem],
        transaction: SDSAnyReadTransaction
    ) throws -> ForwardMessageContent {
        let items: [Item] = try selectionItems.map { selectionItem in
            let interactionId = selectionItem.interactionId
            guard let interaction = TSInteraction.anyFetch(uniqueId: interactionId,
                                                           transaction: transaction) else {
                throw ForwardError.missingInteraction
            }
            let componentState = try buildComponentState(interaction: interaction,
                                                         transaction: transaction)
            return try Item.build(interaction: interaction,
                                  componentState: componentState,
                                  selectionType: selectionItem.selectionType,
                                  transaction: transaction)
        }
        return build(items: items)
    }

    private static func buildComponentState(interaction: TSInteraction,
                                            transaction: SDSAnyReadTransaction) throws -> CVComponentState {
        guard let componentState = CVLoader.buildStandaloneComponentState(
            interaction: interaction,
            spoilerState: SpoilerRenderState(), // Nothing revealed, doesn't matter.
            transaction: transaction
        ) else {
            throw ForwardError.invalidInteraction
        }
        return componentState
    }
}

// MARK: -

private class ForwardNavigationViewController: OWSNavigationController {
    weak var forwardMessageViewController: ForwardMessageViewController?

}

// MARK: -

private class ForwardPickerViewController: ConversationPickerViewController {
    weak var forwardMessageViewController: ForwardMessageViewController?

    public override func viewDidLoad() {
        super.viewDidLoad()

        updateNavigationItem()
    }

    public func updateNavigationItem() {
        title = OWSLocalizedString("FORWARD_MESSAGE_TITLE",
                                  comment: "Title for the 'forward message(s)' view.")

        navigationItem.leftBarButtonItem = UIBarButtonItem(image: Theme.iconImage(.buttonX),
                                                           style: .plain,
                                                           target: self,
                                                           action: #selector(didPressCancel))
        navigationItem.rightBarButtonItem = UIBarButtonItem(image: Theme.iconImage(.buttonSearch),
                                                            style: .plain,
                                                            target: self,
                                                            action: #selector(didPressSearch))
    }

    @objc
    private func didPressCancel() {
        AssertIsOnMainThread()

        pickerDelegate?.conversationPickerDidCancel(self)
    }

    @objc
    private func didPressSearch() {
        AssertIsOnMainThread()

        forwardMessageViewController?.selectSearchBar()
    }
}
