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

import LibSignalClient
import SafariServices
import SignalServiceKit
import SignalUI

extension ConversationViewController: MessageRequestDelegate {

    func messageRequestViewDidTapBlock(mode: MessageRequestMode) {
        AssertIsOnMainThread()

        switch mode {
        case .none:
            owsFailDebug("Invalid mode.")
        case .contactOrGroupRequest:
            let blockSheet = createBlockThreadActionSheet()
            presentActionSheet(blockSheet)
        case .groupInviteRequest:
            showBlockInviteActionSheet()
        }
    }

    func messageRequestViewDidTapReport() {
        let reportSheet = createReportThreadActionSheet()
        presentActionSheet(reportSheet)
    }

    func showBlockInviteActionSheet() {
        Logger.info("")

        guard let groupThread = thread as? TSGroupThread else {
            owsFailDebug("Invalid thread.")
            return
        }

        guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction else {
            owsFailDebug("Missing local identifiers!")
            return
        }

        let groupMembership = groupThread.groupModel.groupMembership

        guard let invitedAtServiceId = groupMembership.localUserInvitedAtServiceId(
            localIdentifiers: localIdentifiers
        ) else {
            owsFailDebug("Can't reject invite if not invited!")
            return
        }

        let actionSheet = ActionSheetController(title: nil, message: nil)

        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "GROUPS_INVITE_BLOCK_GROUP",
                comment: "Label for 'block group' button in group invite view."
            ),
            style: .default
        ) { [weak self] _ in
            self?.blockThread()
        })

        if let addedByAci = groupMembership.addedByAci(forInvitedMember: invitedAtServiceId) {
            let addedByName = databaseStorage.read { tx in
                return contactsManager.displayName(for: SignalServiceAddress(addedByAci), tx: tx).resolvedValue()
            }

            actionSheet.addAction(ActionSheetAction(
                title: String(
                    format: OWSLocalizedString(
                        "GROUPS_INVITE_BLOCK_INVITER_FORMAT",
                        comment: "Label for 'block inviter' button in group invite view. Embeds {{name of user who invited you}}."
                    ),
                    addedByName
                ),
                style: .default
            ) { [weak self] _ in
                self?.blockUserAndDelete(addedByAci)
            })

            actionSheet.addAction(ActionSheetAction(
                title: String(
                    format: OWSLocalizedString(
                        "GROUPS_INVITE_BLOCK_GROUP_AND_INVITER_FORMAT",
                        comment: "Label for 'block group and inviter' button in group invite view. Embeds {{name of user who invited you}}."),
                    addedByName
                ),
                style: .default
            ) { [weak self] _ in
                self?.blockUserAndGroupAndDelete(addedByAci)
            })
        }

        actionSheet.addAction(OWSActionSheets.cancelAction)

        presentActionSheet(actionSheet)
    }

    func blockThread() {
        // Leave the group while blocking the thread.
        databaseStorage.write { transaction in
            blockingManager.addBlockedThread(thread,
                                             blockMode: .localShouldLeaveGroups,
                                             transaction: transaction)
        }
        syncManager.sendMessageRequestResponseSyncMessage(thread: thread,
                                                          responseType: .block)
        NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)
    }

    func blockThreadAndDelete() {
        // Do not leave the group while blocking the thread; we'll
        // that below so that we can surface an error to the user
        // if leaving the group fails.
        databaseStorage.write { transaction in
            blockingManager.addBlockedThread(thread,
                                             blockMode: .localShouldNotLeaveGroups,
                                             transaction: transaction)
        }
        leaveAndSoftDeleteThread(messageRequestResponseType: .blockAndDelete)
    }

    func blockThreadAndReportSpam(in thread: TSThread) {
        databaseStorage.write { tx in
            ReportSpamUIUtils.blockAndReport(in: thread, tx: tx)
        }

        presentToastCVC(
            OWSLocalizedString(
                "MESSAGE_REQUEST_SPAM_REPORTED_AND_BLOCKED",
                comment: "String indicating that spam has been reported and the chat has been blocked."
            )
        )
        NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)
    }

    func reportSpamInThread() {
        databaseStorage.write { tx in
            ReportSpamUIUtils.report(in: thread, tx: tx)
        }

        presentToastCVC(
            OWSLocalizedString(
                "MESSAGE_REQUEST_SPAM_REPORTED",
                comment: "String indicating that spam has been reported."
            )
        )
        NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)
    }

    private func blockUserAndDelete(_ aci: Aci) {
        // Do not leave the group while blocking the thread; we'll
        // that below so that we can surface an error to the user
        // if leaving the group fails.
        databaseStorage.write { transaction in
            blockingManager.addBlockedAddress(
                SignalServiceAddress(aci),
                blockMode: .localShouldNotLeaveGroups,
                transaction: transaction
            )
        }
        leaveAndSoftDeleteThread(messageRequestResponseType: .delete)
    }

    private func blockUserAndGroupAndDelete(_ aci: Aci) {
        ConversationViewController.databaseStorage.write { transaction in
            if let groupThread = self.thread as? TSGroupThread {
                // Do not leave the group while blocking the thread; we'll
                // that below so that we can surface an error to the user
                // if leaving the group fails.
                self.blockingManager.addBlockedGroup(
                    groupModel: groupThread.groupModel,
                    blockMode: .localShouldNotLeaveGroups,
                    transaction: transaction
                )
            } else {
                owsFailDebug("Invalid thread.")
            }
            self.blockingManager.addBlockedAddress(
                SignalServiceAddress(aci),
                blockMode: .localShouldNotLeaveGroups,
                transaction: transaction
            )
        }
        leaveAndSoftDeleteThread(messageRequestResponseType: .blockAndDelete)
    }

    func messageRequestViewDidTapDelete() {
        AssertIsOnMainThread()
        let deleteSheet = createDeleteThreadActionSheet()
        presentActionSheet(deleteSheet)
    }

    private func leaveAndSoftDeleteThread(
        messageRequestResponseType: OWSSyncMessageRequestResponseType
    ) {
        AssertIsOnMainThread()

        syncManager.sendMessageRequestResponseSyncMessage(
            thread: self.thread,
            responseType: messageRequestResponseType
        )

        let completion = {
            ConversationViewController.databaseStorage.write { transaction in
                DependenciesBridge.shared.threadSoftDeleteManager.softDelete(
                    threads: [self.thread],
                    // We're already sending a sync message about this above!
                    sendDeleteForMeSyncMessage: false,
                    tx: transaction.asV2Write
                )
            }
            self.conversationSplitViewController?.closeSelectedConversation(animated: true)
            NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)
        }

        guard let groupThread = thread as? TSGroupThread,
              groupThread.isLocalUserFullOrInvitedMember else {
            // If we don't need to leave the group, finish up immediately.
            return completion()
        }

        // Leave the group if we're a member.
        GroupManager.leaveGroupOrDeclineInviteAsyncWithUI(groupThread: groupThread, fromViewController: self, success: completion)
    }

    func messageRequestViewDidTapAccept(mode: MessageRequestMode, unblockThread: Bool, unhideRecipient: Bool) {
        AssertIsOnMainThread()

        let thread = self.thread
        let completion = {
            SDSDatabaseStorage.shared.asyncWrite { [weak self] transaction in
                guard let self else { return }
                if unblockThread {
                    self.blockingManager.removeBlockedThread(thread, wasLocallyInitiated: true, transaction: transaction)
                }
                if unhideRecipient, let thread = thread as? TSContactThread {
                    DependenciesBridge.shared.recipientHidingManager.removeHiddenRecipient(
                        thread.contactAddress,
                        wasLocallyInitiated: true,
                        tx: transaction.asV2Write
                    )
                }
                if !unblockThread {
                    // If this is an accept (which also unhides a hidden recipient), not an unblock, we should send a
                    // sync messages telling our other devices that we accepted.
                    self.syncManager.sendMessageRequestResponseSyncMessage(
                        thread: thread,
                        responseType: .accept,
                        transaction: transaction
                    )
                }

                // Whitelist the thread
                self.profileManager.addThread(toProfileWhitelist: thread, transaction: transaction)

                if !thread.isGroupThread {
                    // If this is a contact thread, we should give the
                    // now-unblocked contact our profile key.
                    let profileKeyMessage = OWSProfileKeyMessage(thread: thread, transaction: transaction)
                    let preparedMessage = PreparedOutgoingMessage.preprepared(
                        transientMessageWithoutAttachments: profileKeyMessage
                    )
                    SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
                }

                NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)
            }
        }

        switch mode {
        case .none:
            owsFailDebug("Invalid mode.")
        case .contactOrGroupRequest:
            completion()
        case .groupInviteRequest:
            guard let groupThread = thread as? TSGroupThread else {
                owsFailDebug("Invalid thread.")
                return
            }
            GroupManager.acceptGroupInviteAsync(groupThread, fromViewController: self, success: completion)
        }
    }

    func messageRequestViewDidTapUnblock(mode: MessageRequestMode) {
        AssertIsOnMainThread()

        let threadName: String
        let message: String
        if let groupThread = thread as? TSGroupThread {
            threadName = groupThread.groupNameOrDefault
            message = OWSLocalizedString(
                "BLOCK_LIST_UNBLOCK_GROUP_MESSAGE", comment: "An explanation of what unblocking a group means.")
        } else if let contactThread = thread as? TSContactThread {
            threadName = databaseStorage.read { tx in
                return contactsManager.displayName(for: contactThread.contactAddress, tx: tx).resolvedValue()
            }
            message = OWSLocalizedString(
                "BLOCK_LIST_UNBLOCK_CONTACT_MESSAGE", comment: "An explanation of what unblocking a contact means.")
        } else {
            owsFailDebug("Invalid thread.")
            return
        }

        let title = String(format: OWSLocalizedString("BLOCK_LIST_UNBLOCK_TITLE_FORMAT",
                                                     comment: "A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}."),
                           threadName)

        OWSActionSheets.showConfirmationAlert(
            title: title,
            message: message,
            proceedTitle: OWSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON",
                                            comment: "Button label for the 'unblock' button")
        ) { _ in
            self.messageRequestViewDidTapAccept(mode: mode, unblockThread: true, unhideRecipient: true)
        }
    }

    func messageRequestViewDidTapLearnMore() {
        AssertIsOnMainThread()

        // TODO Message Request: Use right support url. Right now this just links to the profiles FAQ
        guard let url = URL(string: "https://support.signal.org/hc/articles/360007459591") else {
            return owsFailDebug("Invalid url.")
        }
        let safariVC = SFSafariViewController(url: url)
        present(safariVC, animated: true)
    }
}

// MARK: - Action Sheets

extension ConversationViewController {

    func createBlockThreadActionSheet(sheetCompletion: ((Bool) -> Void)? = nil) -> ActionSheetController {
        Logger.info("")

        let actionSheetTitleFormat: String
        let actionSheetMessage: String
        if thread.isGroupThread {
            actionSheetTitleFormat = OWSLocalizedString(
                "MESSAGE_REQUEST_BLOCK_GROUP_TITLE_FORMAT",
                comment: "Action sheet title to confirm blocking a group via a message request. Embeds {{group name}}"
            )
            actionSheetMessage = OWSLocalizedString(
                "MESSAGE_REQUEST_BLOCK_GROUP_MESSAGE",
                comment: "Action sheet message to confirm blocking a group via a message request."
            )
        } else {
            actionSheetTitleFormat = OWSLocalizedString(
                "MESSAGE_REQUEST_BLOCK_CONVERSATION_TITLE_FORMAT",
                comment: "Action sheet title to confirm blocking a contact via a message request. Embeds {{contact name or phone number}}"
            )
            actionSheetMessage = OWSLocalizedString(
                "MESSAGE_REQUEST_BLOCK_CONVERSATION_MESSAGE",
                comment: "Action sheet message to confirm blocking a conversation via a message request."
            )
        }

        let (threadName, hasReportedSpam) = databaseStorage.read { tx in
            let threadName = contactsManager.displayName(for: thread, transaction: tx)
            let finder = InteractionFinder(threadUniqueId: thread.uniqueId)
            let hasReportedSpam = finder.hasUserReportedSpam(transaction: tx)
            return (threadName, hasReportedSpam)
        }
        let actionSheetTitle = String(format: actionSheetTitleFormat, threadName)
        let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage)

        let blockActionTitle = OWSLocalizedString(
            "MESSAGE_REQUEST_BLOCK_ACTION",
            comment: "Action sheet action to confirm blocking a thread via a message request."
        )
        let blockAndDeleteActionTitle = OWSLocalizedString(
            "MESSAGE_REQUEST_BLOCK_AND_DELETE_ACTION",
            comment: "Action sheet action to confirm blocking and deleting a thread via a message request."
        )
        let blockAndReportSpamActionTitle = OWSLocalizedString(
            "MESSAGE_REQUEST_BLOCK_AND_REPORT_SPAM_ACTION",
            comment: "Action sheet action to confirm blocking and reporting spam for a thread via a message request."
        )

        actionSheet.addAction(ActionSheetAction(title: blockActionTitle) { [weak self] _ in
            self?.blockThread()
            sheetCompletion?(true)
        })

        if !hasReportedSpam {
            actionSheet.addAction(ActionSheetAction(title: blockAndReportSpamActionTitle) { [weak self] _ in
                guard let self else { return }
                self.blockThreadAndReportSpam(in: self.thread)
                sheetCompletion?(true)
            })
        } else {
            actionSheet.addAction(ActionSheetAction(title: blockAndDeleteActionTitle) { [weak self] _ in
                self?.blockThreadAndDelete()
                sheetCompletion?(true)
            })
        }

        actionSheet.addAction(ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel, handler: { _ in
            sheetCompletion?(false)
        }))
        return actionSheet
    }

    func createDeleteThreadActionSheet() -> ActionSheetController {
        let actionSheetTitle: String
        let actionSheetMessage: String
        let confirmationText: String

        var isMemberOfGroup = false
        if let groupThread = thread as? TSGroupThread {
            isMemberOfGroup = groupThread.isLocalUserMemberOfAnyKind
        }

        if isMemberOfGroup {
            actionSheetTitle = OWSLocalizedString(
                "MESSAGE_REQUEST_LEAVE_AND_DELETE_GROUP_TITLE",
                comment: "Action sheet title to confirm deleting a group via a message request."
            )
            actionSheetMessage = OWSLocalizedString(
                "MESSAGE_REQUEST_LEAVE_AND_DELETE_GROUP_MESSAGE",
                comment: "Action sheet message to confirm deleting a group via a message request."
            )
            confirmationText = OWSLocalizedString(
                "MESSAGE_REQUEST_LEAVE_AND_DELETE_GROUP_ACTION",
                comment: "Action sheet action to confirm deleting a group via a message request."
            )
        } else { // either 1:1 thread, or a group of which I'm not a member
            actionSheetTitle = OWSLocalizedString(
                "MESSAGE_REQUEST_DELETE_CONVERSATION_TITLE",
                comment: "Action sheet title to confirm deleting a conversation via a message request."
            )
            actionSheetMessage = OWSLocalizedString(
                "MESSAGE_REQUEST_DELETE_CONVERSATION_MESSAGE",
                comment: "Action sheet message to confirm deleting a conversation via a message request."
            )
            confirmationText = OWSLocalizedString(
                "MESSAGE_REQUEST_DELETE_CONVERSATION_ACTION",
                comment: "Action sheet action to confirm deleting a conversation via a message request."
            )
        }

        let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage)
        actionSheet.addAction(ActionSheetAction(title: confirmationText, handler: { _ in
            self.leaveAndSoftDeleteThread(messageRequestResponseType: .delete)
        }))
        actionSheet.addAction(ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel))
        return actionSheet
    }

    // TODO[SPAM]: For groups, fetch the inviter to add to the message
    func createReportThreadActionSheet() -> ActionSheetController {
        return ReportSpamUIUtils.createReportSpamActionSheet(
            for: thread,
            isBlocked: threadViewModel.isBlocked
        )
    }
}

extension ConversationViewController: NameCollisionResolutionDelegate {

    func nameCollisionControllerDidComplete(_ controller: NameCollisionResolutionViewController, dismissConversationView: Bool) {
        if dismissConversationView {
            // This may have already been closed (e.g. if the user requested deletion), but
            // it's not guaranteed (e.g. the user blocked the request). Let's close it just
            // to be safe.
            self.conversationSplitViewController?.closeSelectedConversation(animated: false)
        } else {
            // Conversation view is being kept around. Update the banner state to account for any changes
            ensureBannerState()
        }
        controller.dismiss(animated: true, completion: nil)
    }
}
