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

import SignalCoreKit

public enum InteractionDelete {
    /// Specifies the desired side effects of deleting interactions.
    public struct SideEffects {
        /// Specifies what should happen with the ``CallRecord`` associated with
        /// a ``TSInteraction`` being deleted, if one exists.
        public enum AssociatedCallDeleteBehavior {
            /// Delete any ``CallRecord`` associated with the interaction, and
            /// send a `CallEvent` sync message about that deletion.
            case localDeleteAndSendCallEventSyncMessage

            /// Delete any ``CallRecord`` associated with the interaction.
            case localDeleteOnly
        }

        /// Specifies behavior for updating the thread associated with an
        /// interaction when that interaction is deleted.
        public enum UpdateThreadOnInteractionDeleteBehavior {
            /// Update the thread after the interaction is deleted.
            case updateOnEachDeletedInteraction

            /// Skip updating the thread. This value should be used to suppress
            /// intermediate thread updates during a bulk interaction delete.
            case doNotUpdate
        }

        /// Specifies behavior for sending a `DeleteForMe` sync message for any
        /// deleted interactions.
        public enum DeleteForMeSyncMessageBehavior {
            /// Send a sync message.
            /// - Important
            /// Any interactions this case is applied to must match the given
            /// thread.
            case sendSyncMessage(interactionsThread: TSThread)

            /// Do not send a sync message.
            case doNotSend
        }

        let associatedCallDelete: AssociatedCallDeleteBehavior
        let updateThreadOnInteractionDelete: UpdateThreadOnInteractionDeleteBehavior
        let deleteForMeSyncMessage: DeleteForMeSyncMessageBehavior

        private init(
            associatedCallDelete: AssociatedCallDeleteBehavior,
            updateThreadOnInteractionDelete: UpdateThreadOnInteractionDeleteBehavior,
            deleteForMeSyncMessage: DeleteForMeSyncMessageBehavior
        ) {
            self.associatedCallDelete = associatedCallDelete
            self.updateThreadOnInteractionDelete = updateThreadOnInteractionDelete
            self.deleteForMeSyncMessage = deleteForMeSyncMessage
        }

        public static func `default`() -> SideEffects {
            return .custom()
        }

        public static func custom(
            associatedCallDelete: AssociatedCallDeleteBehavior = .localDeleteAndSendCallEventSyncMessage,
            updateThreadOnInteractionDelete: UpdateThreadOnInteractionDeleteBehavior = .updateOnEachDeletedInteraction,
            deleteForMeSyncMessage: DeleteForMeSyncMessageBehavior = .doNotSend
        ) -> SideEffects {
            return SideEffects(
                associatedCallDelete: associatedCallDelete,
                updateThreadOnInteractionDelete: updateThreadOnInteractionDelete,
                deleteForMeSyncMessage: deleteForMeSyncMessage
            )
        }
    }
}

/// Responsible for deleting ``TSInteraction``s, and initiating ``CallRecord``
/// deletion.
///
/// - Note
/// Every ``CallRecord`` is associated with a ``TSInteraction``, and when
/// one is deleted the other should be as well.
///
/// Correspondingly, this manager also provides an entrypoint for callers to
/// delete call records alongside their associated interactions. This may seem
/// counterintuitive, but avoids a circular dependency between interaction and
/// call record deletion.
///
/// - SeeAlso
/// If you're calling this type for a user-initiated deletion, consider using
/// ``DeleteForMeInfoSheetCoordinator`` in the Signal target instead, which
/// handles some one-time informational UX.
public protocol InteractionDeleteManager {
    typealias SideEffects = InteractionDelete.SideEffects

    /// Remove the given interactions.
    func delete(
        interactions: [TSInteraction],
        sideEffects: SideEffects,
        tx: any DBWriteTransaction
    )

    /// Deletes the given call records and their associated interactions.
    func delete(
        alongsideAssociatedCallRecords callRecords: [CallRecord],
        sideEffects: SideEffects,
        tx: any DBWriteTransaction
    )
}

public extension InteractionDeleteManager {
    /// Remove the given interaction.
    func delete(
        _ interaction: TSInteraction,
        sideEffects: SideEffects,
        tx: any DBWriteTransaction
    ) {
        delete(interactions: [interaction], sideEffects: sideEffects, tx: tx)
    }
}

final class InteractionDeleteManagerImpl: InteractionDeleteManager {
    private let callRecordStore: CallRecordStore
    private let callRecordDeleteManager: CallRecordDeleteManager
    private let databaseStorage: SDSDatabaseStorage
    private let deleteForMeOutgoingSyncMessageManager: DeleteForMeOutgoingSyncMessageManager
    private let interactionReadCache: InteractionReadCache
    private let interactionStore: InteractionStore
    private let mediaGalleryResourceManager: MediaGalleryResourceManager
    private let messageSendLog: MessageSendLog

    init(
        callRecordStore: CallRecordStore,
        callRecordDeleteManager: CallRecordDeleteManager,
        databaseStorage: SDSDatabaseStorage,
        deleteForMeOutgoingSyncMessageManager: DeleteForMeOutgoingSyncMessageManager,
        interactionReadCache: InteractionReadCache,
        interactionStore: InteractionStore,
        mediaGalleryResourceManager: MediaGalleryResourceManager,
        messageSendLog: MessageSendLog
    ) {
        self.callRecordStore = callRecordStore
        self.callRecordDeleteManager = callRecordDeleteManager
        self.databaseStorage = databaseStorage
        self.deleteForMeOutgoingSyncMessageManager = deleteForMeOutgoingSyncMessageManager
        self.interactionReadCache = interactionReadCache
        self.interactionStore = interactionStore
        self.mediaGalleryResourceManager = mediaGalleryResourceManager
        self.messageSendLog = messageSendLog
    }

    func delete(
        interactions: [TSInteraction],
        sideEffects: SideEffects,
        tx: any DBWriteTransaction
    ) {
        for interaction in interactions {
            guard interaction.shouldBeSaved else {
                return
            }

            _deleteInternal(
                interaction: interaction,
                knownAssociatedCallRecord: nil,
                sideEffects: sideEffects,
                tx: SDSDB.shimOnlyBridge(tx)
            )
        }

        sendDeleteForMeSyncMessageIfNecessary(
            interactions: interactions,
            sideEffects: sideEffects,
            tx: tx
        )
    }

    func delete(
        alongsideAssociatedCallRecords callRecords: [CallRecord],
        sideEffects: SideEffects,
        tx: any DBWriteTransaction
    ) {
        var deletedInteractions = [TSInteraction]()
        for callRecord in callRecords {
            guard
                let associatedInteraction: TSInteraction = interactionStore
                    .fetchAssociatedInteraction(callRecord: callRecord, tx: tx)
            else { continue }

            deletedInteractions.append(associatedInteraction)
            CallRecord.assertDebugIsCallRecordInteraction(associatedInteraction)

            _deleteInternal(
                interaction: associatedInteraction,
                knownAssociatedCallRecord: callRecord,
                sideEffects: sideEffects,
                tx: SDSDB.shimOnlyBridge(tx)
            )
        }

        sendDeleteForMeSyncMessageIfNecessary(
            interactions: deletedInteractions,
            sideEffects: sideEffects,
            tx: tx
        )
    }

    private func sendDeleteForMeSyncMessageIfNecessary(
        interactions: [TSInteraction],
        sideEffects: SideEffects,
        tx: any DBWriteTransaction
    ) {
        switch sideEffects.deleteForMeSyncMessage {
        case .sendSyncMessage(let interactionsThread):
            owsAssert(
                interactions.allSatisfy { $0.uniqueThreadId == interactionsThread.uniqueId },
                "Thread did not match interaction!"
            )

            deleteForMeOutgoingSyncMessageManager.send(
                deletedInteractions: interactions,
                thread: interactionsThread,
                tx: tx
            )
        case .doNotSend:
            break
        }
    }

    // MARK: -

    private func _deleteInternal(
        interaction: TSInteraction,
        knownAssociatedCallRecord: CallRecord?,
        sideEffects: SideEffects,
        tx: SDSAnyWriteTransaction
    ) {
        willRemove(
            interaction: interaction,
            knownAssociatedCallRecord: knownAssociatedCallRecord,
            sideEffects: sideEffects,
            tx: tx
        )

        tx.unwrapGrdbWrite.executeAndCacheStatement(
            sql: "DELETE FROM model_TSInteraction WHERE uniqueId = ?",
            arguments: [interaction.uniqueId]
        )

        didRemove(
            interaction: interaction,
            sideEffects: sideEffects,
            tx: tx
        )
    }

    private func willRemove(
        interaction: TSInteraction,
        knownAssociatedCallRecord: CallRecord?,
        sideEffects: SideEffects,
        tx: SDSAnyWriteTransaction
    ) {
        databaseStorage.updateIdMapping(interaction: interaction, transaction: tx)

        if
            let callInteraction = interaction as? CallRecordAssociatedInteraction,
            let interactionRowId = callInteraction.sqliteRowId,
            let associatedCallRecord = knownAssociatedCallRecord ?? callRecordStore.fetch(
                interactionRowId: interactionRowId, tx: tx.asV2Read
            )
        {
            let sendSyncMessage = switch sideEffects.associatedCallDelete {
            case .localDeleteOnly: false
            case .localDeleteAndSendCallEventSyncMessage: true
            }

            callRecordDeleteManager.deleteCallRecord(
                associatedCallRecord,
                sendSyncMessageOnDelete: sendSyncMessage,
                tx: tx.asV2Write
            )
        }

        if let message = interaction as? TSMessage {
            // Ensure any associated edits are removed before removing.
            message.removeEdits(transaction: tx)

            if
                let sticker = message.messageSticker,
                interactionStore.exists(uniqueId: message.uniqueId, tx: tx.asV2Write)
            {
                // StickerManager does ref-counting of known sticker packs. If this
                // message is persisted – i.e., if there will in fact be a deletion
                // downstream of this call – we should make sure that refcount gets
                // updated.
                //
                // In a better world we wouldn't be unsure if the message was
                // actually going to be deleted when we call this, but that's a
                // problem for another day.
                StickerManager.removeKnownStickerInfo(sticker.info, transaction: tx)
            }
        }
    }

    private func didRemove(
        interaction: TSInteraction,
        sideEffects: SideEffects,
        tx: SDSAnyWriteTransaction
    ) {
        switch sideEffects.updateThreadOnInteractionDelete {
        case .updateOnEachDeletedInteraction:
            if let associatedThread = interaction.thread(tx: tx) {
                associatedThread.update(withRemovedMessage: interaction, transaction: tx)
            }
        case .doNotUpdate:
            break
        }

        messageSendLog.deleteAllPayloadsForInteraction(interaction, tx: tx)
        interactionReadCache.didRemove(interaction: interaction, transaction: tx)

        if let message = interaction as? TSMessage {
            FullTextSearchIndexer.delete(message, tx: tx)

            if !message.attachmentIds.isEmpty {
                mediaGalleryResourceManager.didRemove(message: message, tx: tx.asV2Write)
            }

            message.removeAllAttachments(tx: tx)
            message.removeAllReactions(transaction: tx)
            message.removeAllMentions(transaction: tx)
            message.touchStoryMessageIfNecessary(replyCountIncrement: .replyDeleted, transaction: tx)
        }
    }
}

// MARK: - Mock

#if TESTABLE_BUILD

open class MockInteractionDeleteManager: InteractionDeleteManager {
    var deleteInteractionsMock: ((
        _ interactions: [TSInteraction],
        _ sideEffects: SideEffects
    ) -> Void)?
    open func delete(interactions: [TSInteraction], sideEffects: SideEffects, tx: any DBWriteTransaction) {
        deleteInteractionsMock!(interactions, sideEffects)
    }

    var deleteAlongsideCallRecordsMock: ((
        _ callRecords: [CallRecord],
        _ sideEffects: SideEffects
    ) -> Void)?
    open func delete(alongsideAssociatedCallRecords callRecords: [CallRecord], sideEffects: SideEffects, tx: any DBWriteTransaction) {
        deleteAlongsideCallRecordsMock!(callRecords, sideEffects)
    }
}

#endif
