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

import Foundation

extension TSInteraction {

    override public func anyDidInsert(with tx: DBWriteTransaction) {
        super.anyDidInsert(with: tx)

        if let thread = thread(tx: tx) {
            thread.updateWithInsertedInteraction(self, tx: tx)
        }
    }

    override public func anyDidUpdate(with tx: DBWriteTransaction) {
        let interactionReadCache = SSKEnvironment.shared.modelReadCachesRef.interactionReadCache

        super.anyDidUpdate(with: tx)

        if let thread = thread(tx: tx) {
            thread.updateWithUpdatedInteraction(self, tx: tx)
        }

        interactionReadCache.didUpdate(interaction: self, transaction: tx)
    }

    // MARK: -

    @objc
    public func fillInMissingSortIdForJustInsertedInteraction(transaction: DBReadTransaction) {
        guard self.sortId == 0 else {
            owsFailDebug("Unexpected sortId: \(sortId).")
            return
        }
        guard
            let sortId = BaseModel.grdbIdByUniqueId(
                tableMetadata: TSInteractionSerializer.table,
                uniqueIdColumnName: InteractionRecord.columnName(.uniqueId),
                uniqueIdColumnValue: self.uniqueId,
                transaction: transaction,
            )
        else {
            owsFailDebug("Missing sortId.")
            return
        }
        guard sortId > 0, sortId <= UInt64.max else {
            owsFailDebug("Invalid sortId: \(sortId).")
            return
        }
        self.replaceSortId(UInt64(sortId))
        owsAssertDebug(self.sortId > 0)
    }

    /// Returns `true` if the receiver was inserted into the database by updating the placeholder
    /// Returns `false` if the receiver needs to be inserted into the database.
    private func updatePlaceholder(
        from sender: SignalServiceAddress,
        transaction: DBWriteTransaction,
    ) -> Bool {
        let placeholders: [OWSRecoverableDecryptionPlaceholder]
        do {
            placeholders = try InteractionFinder.fetchInteractions(
                timestamp: timestamp,
                transaction: transaction,
            ).compactMap { candidate -> OWSRecoverableDecryptionPlaceholder? in
                guard let placeholder = candidate as? OWSRecoverableDecryptionPlaceholder else {
                    return nil
                }
                guard placeholder.sender == sender, placeholder.timestamp == self.timestamp else {
                    return nil
                }
                return placeholder
            }
        } catch {
            owsFailDebug("Failed to fetch placeholder interaction: \(error)")
            return false
        }

        guard let placeholder = placeholders.first else {
            return false
        }

        Logger.info("Fetched placeholder with timestamp: \(timestamp) from sender: \(sender). Performing replacement...")

        if placeholder.supportsReplacement {
            placeholder.replaceWithInteraction(self, writeTx: transaction)
            return true
        } else {
            Logger.info("Placeholder not eligible for replacement, deleting.")
            DependenciesBridge.shared.interactionDeleteManager
                .delete(placeholder, sideEffects: .default(), tx: transaction)
            return false
        }
    }

    @objc
    public func insertOrReplacePlaceholder(from sender: SignalServiceAddress, transaction: DBWriteTransaction) {
        if updatePlaceholder(from: sender, transaction: transaction) {
            Logger.info("Successfully replaced placeholder with interaction: \(timestamp)")
        } else {
            anyInsert(transaction: transaction)

            // Replaced interactions will inherit the existing sortId
            // Inserted interactions will be assigned a sortId from SQLite, but
            // we need to fetch from the database.
            owsAssertDebug(sortId == 0)
            fillInMissingSortIdForJustInsertedInteraction(transaction: transaction)
            owsAssertDebug(sortId > 0)
        }
    }
}

// MARK: - shouldAppearInInbox

extension TSInteraction {

    /// Returns whether the given interaction should pull a conversation to the top of the list
    ///
    /// In most cases, this is equivalent to checking shouldAppearInInbox. But in some cases, we may want to show
    /// a message in the chat list but not bump the thread to the top.

    @objc
    public func shouldBumpThreadToTopOfChatList(transaction: DBReadTransaction) -> Bool {
        switch self {
        case let infoMessage as TSInfoMessage:
            switch infoMessage.messageType {
            case
                .typePinnedMessage,
                .blockedOtherUser,
                .blockedGroup,
                .unblockedOtherUser,
                .unblockedGroup:
                return false
            default:
                return shouldAppearInInbox(transaction: transaction)
            }
        default:
            return shouldAppearInInbox(transaction: transaction)
        }
    }

    /// Returns whether the given interaction should appear in the chat list preview.
    ///
    /// This operation necessarily happens after the interaction has been pulled out of the
    /// database. If possible, they should also be filtered as part of the database queries in the
    /// `mostRecentInteractionForInbox(transaction:)` implementations in InteractionFinder.swift.
    @objc
    public func shouldAppearInInbox(transaction: DBReadTransaction) -> Bool {
        return shouldAppearInInbox(groupUpdateItemsBuilder: { infoMessage in
            guard
                let localIdentifiers = DependenciesBridge.shared.tsAccountManager
                    .localIdentifiers(tx: transaction),
                let updates = infoMessage.computedGroupUpdateItems(
                    localIdentifiers: localIdentifiers,
                    tx: transaction,
                )
            else {
                return nil
            }
            return updates
        })
    }

    /// Returns whether the given interaction should show in the chat list preview
    ///
    /// - parameter groupUpdateItemsBuilder: If the message is a group update info message,
    /// a block that builds the PersistableGroupUpdateItems for the message, which is run synchronously
    /// and may make use of a transaction if needed.
    public func shouldAppearInInbox(
        groupUpdateItemsBuilder: (TSInfoMessage) -> [TSInfoMessage.PersistableGroupUpdateItem]?,
    ) -> Bool {
        if !shouldBeSaved || isDynamicInteraction || self is OutgoingSyncMessage {
            owsFailDebug("Unexpected interaction type: \(type(of: self))")
            return false
        }

        switch self {
        case let errorMessage as TSErrorMessage:
            return Self.shouldErrorMessageAppearInInbox(errorMessage)
        case let infoMessage as TSInfoMessage:
            return Self.shouldInfoMessageAppearInInbox(
                infoMessage,
                groupUpdateItemsBuilder: groupUpdateItemsBuilder,
            )
        case let message as TSMessage:
            return Self.shouldMessageAppearInInbox(message)
        default:
            return true
        }
    }

    private static func shouldErrorMessageAppearInInbox(_ message: TSErrorMessage) -> Bool {
        switch message.errorType {
        case .nonBlockingIdentityChange:
            // Otherwise all group threads with the recipient will percolate to the top of the inbox, even though
            // there was no meaningful interaction.
            return false
        case .decryptionFailure:
            if message is OWSRecoverableDecryptionPlaceholder {
                // Replaceable interactions should never be shown to the user
                return false
            } else {
                return true
            }
        default:
            return true
        }
    }

    private static func shouldMessageAppearInInbox(_ message: TSMessage) -> Bool {
        owsPrecondition(!(message is TSErrorMessage))
        owsPrecondition(!(message is TSInfoMessage))
        // skip considering this message if it's a group story reply, or a past edit revision
        return !message.isGroupStoryReply && !message.isPastEditRevision()
    }

    private static func shouldInfoMessageAppearInInbox(
        _ message: TSInfoMessage,
        groupUpdateItemsBuilder: (TSInfoMessage) -> [TSInfoMessage.PersistableGroupUpdateItem]?,
    ) -> Bool {
        switch message.messageType {
        case .verificationStateChange: return false
        case .profileUpdate: return false
        case .phoneNumberChange: return false
        case .recipientHidden: return false
        case .threadMerge: return false
        case .sessionSwitchover: return false
        case .reportedSpam: return false
        case .learnedProfileName: return false
        case .acceptedMessageRequest: return false
        case .typeGroupUpdate:
            guard
                let updates = groupUpdateItemsBuilder(message)
            else {
                return true
            }
            return updates.contains { update in
                switch update {
                case .wasMigrated, .localUserLeft, .otherUserLeft:
                    return false
                default:
                    return true
                }
            }
        case .typeLocalUserEndedSession: return true
        case .typeRemoteUserEndedSession: return true
        case .userNotRegistered: return true
        case .typeUnsupportedMessage: return true
        case .typeGroupQuit: return true
        case .typeDisappearingMessagesUpdate: return true
        case .addToContactsOffer: return true
        case .addUserToProfileWhitelistOffer: return true
        case .addGroupToProfileWhitelistOffer: return true
        case .unknownProtocolVersion: return true
        case .userJoinedSignal: return true
        case .syncedThread: return true
        case .paymentsActivationRequest: return true
        case .paymentsActivated: return true
        case .blockedOtherUser: return true
        case .blockedGroup: return true
        case .unblockedOtherUser: return true
        case .unblockedGroup: return true
        case .typeEndPoll: return true
        case .typePinnedMessage: return true
        }
    }
}
