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

import Foundation
import SignalServiceKit

extension ThreadUtil {
    // MARK: - Durable Message Enqueue

    public class func enqueueMessage(
        body messageBody: MessageBody?,
        mediaAttachments: [SignalAttachment] = [],
        thread: TSThread,
        quotedReplyDraft: DraftQuotedReplyModel? = nil,
        linkPreviewDraft: OWSLinkPreviewDraft? = nil,
        persistenceCompletionHandler persistenceCompletion: PersistenceCompletion? = nil
    ) {
        AssertIsOnMainThread()

        let messageTimestamp = Date.ows_millisecondTimestamp()

        let benchEventId = sendMessageBenchEventStart(messageTimestamp: messageTimestamp)
        self.enqueueSendQueue.async {
            let unpreparedMessage: UnpreparedOutgoingMessage
            do {
                let messageBody = try messageBody.map {
                    try DependenciesBridge.shared.tsResourceContentValidator
                        .prepareOversizeTextIfNeeded(from: $0)
                } ?? nil
                let linkPreviewDataSource = try linkPreviewDraft.map {
                    try DependenciesBridge.shared.linkPreviewManager.buildDataSource(from: $0)
                }
                let mediaAttachments = try mediaAttachments.map {
                    try $0.forSending()
                }
                let quotedReplyDraft = try quotedReplyDraft.map {
                    try DependenciesBridge.shared.quotedReplyManager.prepareDraftForSending($0)
                }

                unpreparedMessage = Self.databaseStorage.read { readTransaction in
                    UnpreparedOutgoingMessage.build(
                        thread: thread,
                        timestamp: messageTimestamp,
                        messageBody: messageBody,
                        mediaAttachments: mediaAttachments,
                        quotedReplyDraft: quotedReplyDraft,
                        linkPreviewDataSource: linkPreviewDataSource,
                        transaction: readTransaction
                    )
                }
            } catch {
                owsFailDebug("Failed to build message")
                return
            }

            Self.enqueueMessageSync(
                unpreparedMessage,
                benchEventId: benchEventId,
                thread: thread,
                persistenceCompletionHandler: persistenceCompletion
            )
        }
    }

    public class func enqueueEditMessage(
        body messageBody: MessageBody?,
        thread: TSThread,
        quotedReplyEdit: MessageEdits.Edit<Void>,
        linkPreviewDraft: OWSLinkPreviewDraft?,
        editTarget: TSOutgoingMessage,
        persistenceCompletionHandler persistenceCompletion: PersistenceCompletion? = nil
    ) {
        AssertIsOnMainThread()

        let messageTimestamp = Date.ows_millisecondTimestamp()

        let benchEventId = sendMessageBenchEventStart(messageTimestamp: messageTimestamp)
        self.enqueueSendQueue.async {
            let unpreparedMessage: UnpreparedOutgoingMessage
            do {
                let messageBody = try messageBody.map {
                    try DependenciesBridge.shared.tsResourceContentValidator
                        .prepareOversizeTextIfNeeded(from: $0)
                } ?? nil
                let linkPreviewDataSource = try linkPreviewDraft.map {
                    try DependenciesBridge.shared.linkPreviewManager.buildDataSource(from: $0)
                }

                unpreparedMessage = UnpreparedOutgoingMessage.buildForEdit(
                    thread: thread,
                    timestamp: messageTimestamp,
                    messageBody: messageBody,
                    quotedReplyEdit: quotedReplyEdit,
                    linkPreviewDataSource: linkPreviewDataSource,
                    editTarget: editTarget
                )
            } catch {
                owsFailDebug("Failed to build message")
                return
            }

            Self.enqueueMessageSync(
                unpreparedMessage,
                benchEventId: benchEventId,
                thread: thread,
                persistenceCompletionHandler: persistenceCompletion
            )
        }
    }

    // MARK: - Durable Message Enqueue

    class func enqueueMessage(
        _ unpreparedMessage: UnpreparedOutgoingMessage,
        thread: TSThread,
        persistenceCompletionHandler persistenceCompletion: PersistenceCompletion? = nil
    ) {
        let benchEventId = sendMessageBenchEventStart(messageTimestamp: unpreparedMessage.messageTimestampForLogging)
        self.enqueueSendQueue.async {
            Self.enqueueMessageSync(
                unpreparedMessage,
                benchEventId: benchEventId,
                thread: thread,
                persistenceCompletionHandler: persistenceCompletion
            )
        }
    }

    /// WARNING: MUST be called on enqueueSendQueue!
    private class func enqueueMessageSync(
        _ unpreparedMessage: UnpreparedOutgoingMessage,
        benchEventId: String,
        thread: TSThread,
        persistenceCompletionHandler persistenceCompletion: PersistenceCompletion? = nil
    ) {
        assertOnQueue(Self.enqueueSendQueue)
        Self.databaseStorage.write { writeTransaction in
            guard let preparedMessage = try? unpreparedMessage.prepare(tx: writeTransaction) else {
                owsFailDebug("Failed to prepare message")
                return
            }
            let promise = SSKEnvironment.shared.messageSenderJobQueueRef.add(
                .promise,
                message: preparedMessage,
                transaction: writeTransaction
            )
            if let persistenceCompletion = persistenceCompletion {
                writeTransaction.addAsyncCompletionOnMain {
                    persistenceCompletion()
                }
            }
            _ = promise.done(on: DispatchQueue.global()) {
                BenchEventComplete(eventId: benchEventId)
            }

            if
                let messageForIntent = preparedMessage.messageForIntentDonation(tx: writeTransaction),
                let thread = messageForIntent.thread(tx: writeTransaction)
            {
                thread.donateSendMessageIntent(for: messageForIntent, transaction: writeTransaction)
            }
        }
    }

    private static func sendMessageBenchEventStart(messageTimestamp: UInt64) -> String {
        let eventId = "sendMessageMarkedAsSent-\(messageTimestamp)"
        BenchEventStart(
            title: "Send Message Milestone: Marked as Sent (\(messageTimestamp))",
            eventId: eventId,
            logInProduction: true
        )
        return eventId
    }
}

// MARK: -

extension UnpreparedOutgoingMessage {

    public static func build(
        thread: TSThread,
        timestamp: UInt64? = nil,
        messageBody: ValidatedTSMessageBody?,
        mediaAttachments: [SignalAttachment.ForSending] = [],
        quotedReplyDraft: DraftQuotedReplyModel.ForSending?,
        linkPreviewDataSource: LinkPreviewTSResourceDataSource?,
        transaction: SDSAnyReadTransaction
    ) -> UnpreparedOutgoingMessage {

        let truncatedBody: MessageBody?
        let oversizeTextDataSource: OversizeTextDataSource?
        switch messageBody {
        case .inline(let messageBody):
            truncatedBody = messageBody
            oversizeTextDataSource = nil
        case .oversize(let truncated, let fullsize):
            truncatedBody = truncated
            oversizeTextDataSource = fullsize
        case nil:
            truncatedBody = nil
            oversizeTextDataSource = nil
        }

        let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
        let expiresInSeconds = dmConfigurationStore.durationSeconds(for: thread, tx: transaction.asV2Read)

        let isVoiceMessage = mediaAttachments.count == 1
            && oversizeTextDataSource == nil
            && mediaAttachments.last?.renderingFlag == .voiceMessage

        var isViewOnceMessage = false
        for attachment in mediaAttachments {
            if attachment.isViewOnce {
                assert(mediaAttachments.count == 1)
                isViewOnceMessage = true
                break
            }

            if attachment.renderingFlag == .borderless {
                assert(mediaAttachments.count == 1)
                break
            }
        }

        let messageBuilder = TSOutgoingMessageBuilder(thread: thread, timestamp: timestamp)

        messageBuilder.messageBody = truncatedBody?.text
        messageBuilder.bodyRanges = truncatedBody?.ranges

        messageBuilder.expiresInSeconds = expiresInSeconds
        messageBuilder.isVoiceMessage = isVoiceMessage
        messageBuilder.isViewOnceMessage = isViewOnceMessage

        let message = messageBuilder.build(transaction: transaction)

        let attachmentInfos = mediaAttachments.map(\.dataSource)

        let unpreparedMessage = UnpreparedOutgoingMessage.forMessage(
            message,
            unsavedBodyMediaAttachments: attachmentInfos,
            oversizeTextDataSource: oversizeTextDataSource,
            linkPreviewDraft: linkPreviewDataSource,
            quotedReplyDraft: quotedReplyDraft
        )
        return unpreparedMessage
    }

    public static func buildForEdit(
        thread: TSThread,
        timestamp: UInt64,
        messageBody: ValidatedTSMessageBody?,
        quotedReplyEdit: MessageEdits.Edit<Void>,
        linkPreviewDataSource: LinkPreviewTSResourceDataSource?,
        editTarget: TSOutgoingMessage
    ) -> UnpreparedOutgoingMessage {

        let truncatedBody: MessageBody?
        let oversizeTextDataSource: OversizeTextDataSource?
        switch messageBody {
        case .inline(let messageBody):
            truncatedBody = messageBody
            oversizeTextDataSource = nil
        case .oversize(let truncated, let fullsize):
            truncatedBody = truncated
            oversizeTextDataSource = fullsize
        case nil:
            truncatedBody = nil
            oversizeTextDataSource = nil
        }

        let edits = MessageEdits(
            timestamp: timestamp,
            body: .change(truncatedBody?.text),
            bodyRanges: .change(truncatedBody?.ranges)
        )

        let unpreparedMessage = UnpreparedOutgoingMessage.forEditMessage(
            targetMessage: editTarget,
            edits: edits,
            oversizeTextDataSource: oversizeTextDataSource,
            linkPreviewDraft: linkPreviewDataSource,
            quotedReplyEdit: quotedReplyEdit
        )
        return unpreparedMessage
    }
}
