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

import Foundation

/// Wraps a TSOutgoingMessage that:
/// * Is already inserted to the database, if the message type needs inserting
/// * Has had any attachments created and inserted to the database
/// and is therefore prepared for sending.
///
/// Just a wrapper for the message object and metadata needed for sending.
public class PreparedOutgoingMessage {

    // MARK: - Pre-prepared constructors

    /// Use this _only_ for already-inserted persisted messages that we are resending.
    /// No insertion or attachment prep is done; attachments should be inserted (but maybe not uploaded).
    public static func preprepared(
        forResending message: TSOutgoingMessage,
        messageRowId: Int64
    ) -> PreparedOutgoingMessage {
        let messageType = MessageType.persisted(MessageType.Persisted(
            rowId: messageRowId,
            message: message
        ))
        return PreparedOutgoingMessage(messageType: messageType)
    }

    /// Use this _only_ to "prepare" outgoing story messages that already created their attachments.
    /// Instantly prepares because...these messages don't need any preparing.
    public static func preprepared(
        outgoingStoryMessage: OutgoingStoryMessage
    ) -> PreparedOutgoingMessage {
        let messageType = MessageType.story(MessageType.Story(
            message: outgoingStoryMessage
        ))
        return PreparedOutgoingMessage(messageType: messageType)
    }

    /// Use this _only_ to "prepare" outgoing contact sync that, by definition, already uploaded their attachment.
    /// Instantly prepares because...these messages don't need any preparing.
    public static func preprepared(
        contactSyncMessage: OWSSyncContactsMessage
    ) -> PreparedOutgoingMessage {
        let messageType = MessageType.contactSync(contactSyncMessage)
        return PreparedOutgoingMessage(messageType: messageType)
    }

    /// Use this _only_ to "prepare" messages that are:
    /// (1) not saved to the interactions table
    /// AND
    /// (2) don't have any attachments associated with them
    /// Instantly prepares because...these messages don't need any preparing.
    public static func preprepared(
        transientMessageWithoutAttachments: TSOutgoingMessage
    ) -> PreparedOutgoingMessage {
        UnpreparedOutgoingMessage.assertIsAllowedTransientMessage(transientMessageWithoutAttachments)
        let messageType = MessageType.transient(transientMessageWithoutAttachments)
        return PreparedOutgoingMessage(messageType: messageType)
    }

    /// ``MessageSenderJobRecord`` gets created from a prepared message (see that class)
    /// and, after reading the record back into memory from disk, can be restored to a Prepared message.
    ///
    /// Returns nil if the message no longer exists; records keep a pointer to a message which may be since deleted.
    public static func restore(
        from jobRecord: MessageSenderJobRecord,
        tx: SDSAnyReadTransaction
    ) -> PreparedOutgoingMessage? {
        switch jobRecord.messageType {
        case .persisted(let messageId, _):
            guard
                let interaction = TSOutgoingMessage.anyFetch(uniqueId: messageId, transaction: tx),
                let message = interaction as? TSOutgoingMessage
            else {
                return nil
            }
            return .init(messageType: .persisted(.init(rowId: message.sqliteRowId!, message: message)))
        case .editMessage(let editedMessageId, let messageForSending, _):
            guard
                let interaction = TSOutgoingMessage.anyFetch(uniqueId: editedMessageId, transaction: tx),
                let editedMessage = interaction as? TSOutgoingMessage
            else {
                return nil
            }
            return .init(messageType: .editMessage(.init(
                editedMessageRowId: editedMessage.sqliteRowId!,
                editedMessage: editedMessage,
                messageForSending: messageForSending
            )))
        case .transient(let message):
            return .init(messageType: .transient(message))
        case .none:
            return nil
        }
    }

    // MARK: - Message Type

    public enum MessageType {

        /// The message is inserted into the Interactions table, ready for sending.
        case persisted(Persisted)

        /// An edit for an existing message; the original is (already was) persisted to the Interaction table, but is now edited.
        case editMessage(EditMessage)

        /// A contact sync message that is not inserted into the Interactions table.
        /// It has an attachment, but that attachment is never persisted as an Attachment
        /// in the database; it is simply in memory and already uploaded.
        case contactSync(OWSSyncContactsMessage)

        /// An OutgoingStoryMessage: a TSMessage subclass we use for sending a ``StoryMessage``
        /// The StoryMessage is persisted to the StoryMessages table and is the owner for any attachments;
        /// the OutgoingStoryMessage is _not_ persisted to the Interactions table.
        case story(Story)

        /// Catch-all for messages not persisted to the Interactions table. NOT allowed to have attachments.
        case transient(TSOutgoingMessage)

        public struct Persisted {
            public let rowId: Int64
            public let message: TSOutgoingMessage
        }

        public struct EditMessage {
            public let editedMessageRowId: Int64
            public let editedMessage: TSOutgoingMessage
            public let messageForSending: OutgoingEditMessage
        }

        public struct Story {
            public let message: OutgoingStoryMessage

            public var storyMessageRowId: Int64 {
                message.storyMessageRowId
            }
        }
    }

    // MARK: - Public getters

    public var uniqueThreadId: String {
        // The message we send and the message we apply updates to always
        // have the same thread; just use this one for convenience.
        return messageForSendStateUpdates.uniqueThreadId
    }

    public func attachmentIdsForUpload(tx: SDSAnyReadTransaction) -> [TSResourceId] {
        switch messageType {
        case .persisted(let persisted):
            var attachmentIds = DependenciesBridge.shared.tsResourceStore.allAttachments(
                for: persisted.message,
                tx: tx.asV2Read
            ).map(\.resourceId)
            if
                let quoteAttachentInfo = persisted.message.quotedMessage?.attachmentInfo(),
                let quoteLegacyAttachmentId = quoteAttachentInfo.attachmentId
            {
                switch quoteAttachentInfo.attachmentType {
                case .originalForSend, .original, .untrustedPointer:
                    // Only for legacy attachments, ignore "original" and "untrusted pointer"
                    // quote attachments for uploading purposes.
                    attachmentIds.removeAll(where: { $0 == .legacy(uniqueId: quoteLegacyAttachmentId) })
                default:
                    break
                }
            }
            return attachmentIds
        case .editMessage(let editMessage):
            return DependenciesBridge.shared.tsResourceStore.allAttachments(
                for: editMessage.editedMessage,
                tx: tx.asV2Read
            ).map(\.resourceId)
        case .contactSync:
            // These are pre-uploaded.
            return []
        case .story(let story):
            guard let storyMessage = StoryMessage.anyFetch(uniqueId: story.message.storyMessageId, transaction: tx) else {
                return []
            }
            switch storyMessage.attachment {
            case .file, .foreignReferenceAttachment:
                return [
                    DependenciesBridge.shared.tsResourceStore
                        .mediaAttachment(for: storyMessage, tx: tx.asV2Read)?.resourceId
                ].compacted()
            case .text:
                return [
                    DependenciesBridge.shared.tsResourceStore
                        .linkPreviewAttachment(for: storyMessage, tx: tx.asV2Read)?.resourceId
                ].compacted()
            }
        case .transient:
            return []
        }
    }

    public func sendingQueuePriority(tx: SDSAnyReadTransaction) -> Operation.QueuePriority {
        switch messageType {
        case .persisted(let message):
            return message.message.insertedMessageHasRenderableContent(rowId: message.rowId, tx: tx) ? .normal : .low
        case .editMessage, .story:
            // Always have renderable content; send at normal priority.
            return .normal
        case .transient, .contactSync:
            return .low
        }
    }

    /// The message, if any, we should use to donate the ``INSendMessageIntent`` to the OS for sharesheet shortcuts.
    public func messageForIntentDonation(tx: SDSAnyReadTransaction) -> TSOutgoingMessage? {
        switch messageType {
        case .persisted(let persisted):
            if persisted.message.isGroupStoryReply {
                return nil
            }
            guard persisted.message.insertedMessageHasRenderableContent(rowId: persisted.rowId, tx: tx) else {
                return nil
            }
            return persisted.message
        case .editMessage(let editMessage):
            // Edited messages were always renderable.
            return editMessage.editedMessage
        case .contactSync:
            return nil
        case .story:
            // We don't donate story message intents.
            return nil
        case .transient(let message):
            if message is OWSOutgoingReactionMessage {
                return message
            } else {
                return nil
            }
        }
    }

    // MARK: - Sending

    public func send(_ sender: (TSOutgoingMessage) async throws -> Void) async throws {
        try await sender(messageForSending)
    }

    public func attachmentUploadOperations(tx: SDSAnyReadTransaction) -> [Operation] {
        let legacyMessageOwnerId = messageForSending.uniqueId
        return attachmentIdsForUpload(tx: tx).map { attachmentId in
            return AsyncBlockOperation {
                try await DependenciesBridge.shared.tsResourceUploadManager.uploadAttachment(
                    attachmentId: attachmentId,
                    legacyMessageOwnerIds: [legacyMessageOwnerId]
                )
            }
        }
    }

    // MARK: - Message state updates

    public func updateAllUnsentRecipientsAsSending(tx: SDSAnyWriteTransaction) {
        messageForSendStateUpdates.updateAllUnsentRecipientsAsSending(transaction: tx)
    }

    public func updateWithAllSendingRecipientsMarkedAsFailed(tx: SDSAnyWriteTransaction) {
        messageForSendStateUpdates.updateWithAllSendingRecipientsMarkedAsFailed(with: tx)
    }

    public func updateWithSendingError(_ error: Error, tx: SDSAnyWriteTransaction) {
        messageForSendStateUpdates.update(sendingError: error, transaction: tx)
    }

    // MARK: - Persistence

    public func asMessageSenderJobRecord(
        isHighPriority: Bool,
        tx: SDSAnyReadTransaction
    ) throws -> MessageSenderJobRecord {
        switch messageType {
        case .persisted(let persisted):
            return try .init(persistedMessage: persisted, isHighPriority: isHighPriority, transaction: tx)
        case .editMessage(let edit):
            return try .init(editMessage: edit, isHighPriority: isHighPriority, transaction: tx)
        case .contactSync:
            throw OWSAssertionError("Cannot create a job record for contact syncs; they can't be persisted!")
        case .story(let story):
            return .init(storyMessage: story, isHighPriority: isHighPriority)
        case .transient(let message):
            return .init(transientMessage: message, isHighPriority: isHighPriority)
        }
    }

    // MARK: - Private

    fileprivate let messageType: MessageType

    // Can effectively only be called by UnpreparedOutgoingMessage, as only
    // that class can instantiate a builder.
    internal convenience init(_ builder: UnpreparedOutgoingMessage.PreparedMessageBuilder) {
        self.init(messageType: builder.messageType)
    }

    private init(messageType: MessageType) {
        self.messageType = messageType

        let body: String? = {
            switch messageType {
            case .persisted(let message):
                return message.message.body
            case .editMessage(let message):
                return message.editedMessage.body
            case .contactSync:
                return nil
            case .story:
                return nil
            case .transient(let message):
                return message.body
            }
        }()

        if let body {
            owsAssertDebug(body.lengthOfBytes(using: .utf8) <= kOversizeTextMessageSizeThreshold)
        }
    }

    // Private on purpose; too easy to abuse for the variety of fields on
    // TSMessage if exposed.
    private var messageForSending: TSOutgoingMessage {
        switch messageType {
        case .persisted(let message):
            return message.message
        case .editMessage(let message):
            return message.messageForSending
        case .contactSync(let message):
            return message
        case .story(let storyMessage):
            return storyMessage.message
        case .transient(let message):
            return message
        }
    }

    // Private on purpose; too easy to abuse for the variety of fields on
    // TSMessage if exposed.
    private var messageForSendStateUpdates: TSOutgoingMessage {
        switch messageType {
        case .persisted(let message):
            return message.message
        case .editMessage(let message):
            // We update the send state on the _original_ edited message.
            return message.editedMessage
        case .contactSync(let message):
            return message
        case .story(let storyMessage):
            return storyMessage.message
        case .transient(let message):
            // Do send states even matter for transient messages?
            return message
        }
    }
}

extension Array where Element == PreparedOutgoingMessage {

    public func attachmentIdsForUpload(tx: SDSAnyReadTransaction) -> [TSResourceId] {
        // Use a non-story message if we have one.
        // When we multisend N attachments to M message threads and S story threads,
        // we create M messages with N attachments each, and (N * S) story messages
        // with one attachment each. So, prefer a non story message which has
        // all the attachments on it.
        // Fall back to just a single story message; fetching attachments for each
        // one and collating is too expensive.
        var storyMessages = [PreparedOutgoingMessage]()
        for preparedMessage in self {
            switch preparedMessage.messageType {
            case .persisted, .editMessage, .contactSync, .transient:
                return preparedMessage.attachmentIdsForUpload(tx: tx)
            case .story:
                storyMessages.append(preparedMessage)
            }
        }
        var attachmentIds = Set<TSResourceId>()
        storyMessages.forEach { message in
            message.attachmentIdsForUpload(tx: tx).forEach { attachmentIds.insert($0) }
        }
        return [TSResourceId](attachmentIds)
    }
}

extension PreparedOutgoingMessage: CustomStringConvertible {

    public var description: String {
        return "Prepared message \(type(of: messageForSending)), timestamp: \(messageForSending.timestamp)"
    }
}

extension PreparedOutgoingMessage: Equatable {
    public static func == (lhs: PreparedOutgoingMessage, rhs: PreparedOutgoingMessage) -> Bool {
        return lhs.messageForSending.uniqueId == rhs.messageForSending.uniqueId
    }
}

// TODO: remove these methods when we remove multisend; they need to exposed only
// for that use case.
extension PreparedOutgoingMessage {

    public static func preprepared(
        forMultisendOf message: TSOutgoingMessage,
        messageRowId: Int64
    ) -> PreparedOutgoingMessage {
        let messageType = MessageType.persisted(MessageType.Persisted(
            rowId: messageRowId,
            message: message
        ))
        return PreparedOutgoingMessage(messageType: messageType)
    }

    public var storyMessage: OutgoingStoryMessage? {
        switch messageType {
        case .persisted, .editMessage, .contactSync, .transient:
            return nil
        case .story(let storyMessage):
            return storyMessage.message
        }
    }

    public func legacyBodyAttachmentIdsForMultisend() -> [String] {
        switch messageType {
        case .persisted(let persisted):
            return persisted.message.attachmentIds
        case .editMessage:
            owsFailDebug("We shouldn't be multisending an edit?")
            return []
        case .contactSync:
            return []
        case .story:
            owsFailDebug("Body attachment getter shouldn't be used for story messages")
            return []
        case .transient:
            return []
        }
    }
}
