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

import Contacts
import LibSignalClient
import SignalServiceKit

/// If we try and hide a recipient but fail because they correspond to
/// a system contact, we show this controller which provides a hook
/// to delete the system contact (which, if successful, then triggers a hide).
///
/// SHOULD ONLY BE DISPLAYED ON THE PRIMARY DEVICE
class DeleteSystemContactViewController: OWSTableViewController2 {
    /// Dependencies needed by this view controller.
    /// Note that these dependencies can be accessed
    /// as global properties on `OWSTableViewController2`,
    /// but we are moving towards more explicit dependencies.
    private let dependencies: Dependencies
    private struct Dependencies {
        let contactsManager: any ContactManager
        let databaseStorage: SDSDatabaseStorage
        let recipientHidingManager: RecipientHidingManager
        let tsAccountManager: TSAccountManager
    }

    /// The e164 of the contact represented by this contact card.
    private let e164: E164
    private let serviceId: ServiceId?

    /// The view controller that should present the toast
    /// confirming successful contact deletion. Note that
    /// this cannot be `self` because `self` dismisses upon
    /// successful deletion.
    private let viewControllerPresentingToast: UIViewController

    init(
        e164: E164,
        serviceId: ServiceId?,
        viewControllerPresentingToast: UIViewController,
        contactsManager: any ContactManager,
        databaseStorage: SDSDatabaseStorage,
        recipientHidingManager: RecipientHidingManager,
        tsAccountManager: TSAccountManager
    ) {
        self.e164 = e164
        self.serviceId = serviceId
        self.viewControllerPresentingToast = viewControllerPresentingToast
        self.dependencies = Dependencies(
            contactsManager: contactsManager,
            databaseStorage: databaseStorage,
            recipientHidingManager: recipientHidingManager,
            tsAccountManager: tsAccountManager
        )
        super.init()
    }

    private lazy var spinnerContainer: UIView = {
        let view = UIView()
        self.view.addSubview(view)
        view.autoPinEdgesToSuperviewEdges()
        view.backgroundColor = .black.withAlphaComponent(0.15)
        view.addSubview(spinnerView)
        spinnerView.autoCenterInSuperview()
        view.isHidden = true
        return view
    }()

    private lazy var spinnerView = UIActivityIndicatorView()

    override func viewDidLoad() {
        super.viewDidLoad()

        // This screen is for primary devices only. If a non primary
        // manages to get here bad things could happen.
        owsAssert(DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegisteredPrimaryDevice)

        self.navigationItem.leftBarButtonItem = .cancelButton(dismissingFrom: self)
    }

    override func viewWillAppear(_ animated: Bool) {
        updateTableContents()
    }

    private enum Constants {
        /// Width and height for avatar image.
        static let avatarDiameter = CGFloat(112)
        /// Toast inset from bottom of view.
        static let toastInset = 8.0
    }

    /// Cell containing contact avatar.
    ///
    /// - Parameter image: The avatar image.
    private func avatarCell(image: UIImage) -> UITableViewCell {
        let cell = OWSTableItem.newCell()
        cell.selectionStyle = .none
        cell.contentView.backgroundColor = .clear

        let imageView = ContactDeletionAvatarImageView(image: image)
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.setContentCompressionResistancePriority(.required, for: .vertical)

        cell.contentView.addSubview(imageView)
        NSLayoutConstraint.activate([
            imageView.heightAnchor.constraint(equalToConstant: Constants.avatarDiameter),
            imageView.widthAnchor.constraint(equalToConstant: Constants.avatarDiameter),
            cell.contentView.topAnchor.constraint(equalTo: imageView.topAnchor),
            cell.contentView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
            cell.contentView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor)
        ])
        return cell
    }

    /// Ordered array of names to be displayed on the contact card.
    /// Each name should have its own cell in the same section.
    /// If the contact has no associated names, this method returns
    /// an empty array.
    ///
    /// - Parameter nameComponents: The name components of the person whose names
    ///   we will return.
    private func names(systemContactName: DisplayName.SystemContactName?) -> [String] {
        guard let systemContactName else {
            return []
        }
        let config: DisplayName.Config = .current()
        if
            config.shouldUseSystemContactNicknames,
            let nickname = systemContactName.nameComponents.nickname
        {
            return [nickname]
        }
        var names = [String]()
        if let firstName = systemContactName.nameComponents.givenName {
            names.append(firstName)
        }
        if let lastName = systemContactName.nameComponents.familyName {
            names.append(lastName)
        }
        return names
    }

    private func updateTableContents() {
        let contents = OWSTableContents()

        let addressForProfileLookup = SignalServiceAddress(serviceId: serviceId, e164: e164)
        let (
            image,
            systemContactName
        ) = dependencies.databaseStorage.read { tx in
            let image = avatarBuilder.avatarImage(
                forAddress: addressForProfileLookup,
                diameterPixels: Constants.avatarDiameter * UIScreen.main.scale,
                localUserDisplayMode: .asUser,
                transaction: tx
            )
            let systemContactName = dependencies.contactsManager.systemContactName(
                for: e164.stringValue,
                tx: tx
            )
            return (image, systemContactName)
        }

        // Avatar
        let avatarSection = OWSTableSection()
        avatarSection.hasBackground = false
        let avatarItem = OWSTableItem(customCellBlock: { [weak self] in
            if
                let image = image,
                let avatarCell = self?.avatarCell(image: image)
            {
                return avatarCell
            }
            return UITableViewCell()
        })
        avatarSection.add(avatarItem)
        contents.add(avatarSection)

        // Name(s)
        let names = names(systemContactName: systemContactName)
        if !names.isEmpty {
            let nameSection = OWSTableSection()
            names.forEach { name in
                nameSection.add(
                    OWSTableItem.label(
                        withText: name.filterForDisplay,
                        accessoryType: .none
                    )
                )
            }
            contents.add(nameSection)
        }

        // Phone Number
        let formattedPhoneNum = PhoneNumber.bestEffortFormatPartialUserSpecifiedText(toLookLikeAPhoneNumber: e164.stringValue)
        let phoneNumberSection = OWSTableSection()
        phoneNumberSection.add(
            OWSTableItem.label(
                withText: formattedPhoneNum,
                accessoryType: .none
            )
        )
        contents.add(phoneNumberSection)

        // Delete Contact button
        let deleteContactSection = OWSTableSection()
        deleteContactSection.add(
            OWSTableItem(
                customCellBlock: {
                    return OWSTableItem.buildCell(
                        itemName: OWSLocalizedString(
                            "DELETE_CONTACT_BUTTON",
                            comment: "Title of button for deleting system contact."
                        ),
                        textColor: .ows_accentRed,
                        accessoryType: .none
                    )
                },
                actionBlock: { [weak self] in
                    guard let self else { return }
                    let displayName: DisplayName = systemContactName.map { .systemContactName($0) } ?? .unknown
                    self.displayDeleteContactActionSheet(
                        phoneNumber: self.e164.stringValue,
                        displayNameForToast: displayName.resolvedValue().formattedForActionSheetTitle()
                    )
                }
            )
        )
        contents.add(deleteContactSection)

        self.contents = contents
    }

    /// Displays the action sheet confirming that the user really
    /// wants to delete this contact from their system contacts.
    private func displayDeleteContactActionSheet(phoneNumber: String?, displayNameForToast: String) {
        let actionSheet = ActionSheetController(
            title: OWSLocalizedString(
                "DELETE_CONTACT_ACTION_SHEET_TITLE",
                comment: "Title of action sheet confirming the user wants to delete a system contact."
            ),
            message: OWSLocalizedString(
                "DELETE_CONTACT_ACTION_SHEET_EXPLANATION",
                comment: "An explanation of what happens in Signal when you remove a system contact."
            )
        )

        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "DELETE_CONTACT_ACTION_SHEET_BUTTON",
                comment: "'Delete' button label on the delete contact confirmation action sheet"
            ),
            style: .destructive,
            handler: { [weak self] _ in
                self?.handleContactDelete(displayNameForToast: displayNameForToast)
            }
        ))
        actionSheet.addAction(ActionSheetAction(
            title: CommonStrings.cancelButton,
            style: .cancel
        ))
        self.presentActionSheet(actionSheet)
    }

    private func handleContactDelete(displayNameForToast: String) {
        switch CNContactStore.authorizationStatus(for: .contacts) {
        case .authorized:
            break
        case .notDetermined, .denied, .restricted:
            fallthrough
        @unknown default:
            Logger.info("No contact permissions")
            showGenericErrorToastAndDismiss()
        }

        let signalAccount = self.dependencies.databaseStorage.read { tx in
            return SignalAccountFinder().signalAccount(
                for: self.e164,
                tx: tx
            )
        }
        // In the case where we have more than one contact with the e164,
        // prefer the one with this id. Otherwise, choice is arbitrary.
        let preferredContactIdForDeletion = signalAccount?.cnContactId

        // Go to CNContacts as the source of truth for contacts.
        let contactStore = CNContactStore()
        let phoneNumPredicate = CNContact.predicateForContacts(matching: CNPhoneNumber(stringValue: e164.stringValue))
        guard
            let contacts = try? contactStore.unifiedContacts(
                    matching: phoneNumPredicate,
                    keysToFetch: [CNContactIdentifierKey as CNKeyDescriptor]
                )
        else {
            Logger.error("Failed to fetch CNContacts!")
            showGenericErrorToastAndDismiss()
            return
        }
        var contactToDelete: CNContact?
        if let preferredContactIdForDeletion {
            contactToDelete = contacts.first(where: { $0.identifier == preferredContactIdForDeletion })
        }
        if contactToDelete == nil {
            contactToDelete = contacts.first
        }

        guard let contactToDelete = contactToDelete?.mutableCopy() as? CNMutableContact else {
            // No contact to delete! Done.
            Logger.warn("No contact to delete, exiting early.")
            showGenericErrorToastAndDismiss()
            return
        }

        var didFail = false
        // Set up the observer _before_ deleting, so its around
        // when the deletion happens.
        NotificationCenter.default.observe(once: .OWSContactsManagerSignalAccountsDidChange)
            .asVoid()
            .timeout(on: DispatchQueue.main, seconds: 5, substituteValue: ())
            .observe(on: DispatchQueue.main) { [weak self] _ in
                guard let self, !didFail else { return }

                defer {
                    self.dismiss(animated: true)
                }

                // Check that the contact got deleted from our db.
                let isStillSystemContact = self.contactsManager.cnContactId(for: self.e164.stringValue) != nil
                if isStillSystemContact {
                    // Can't hide; likely there was another contact with the same number.
                    // Just exit.
                    Logger.warn("Address still a system contact after deletion; possibly duplicate system contact")
                    return
                }
                self.dependencies.databaseStorage.write { tx in
                    do {
                        try self.dependencies.recipientHidingManager.addHiddenRecipient(
                            SignalServiceAddress(serviceId: self.serviceId, e164: self.e164),
                            wasLocallyInitiated: true,
                            tx: tx.asV2Write
                        )
                        self.displayDeletedContactToast(displayNameForToast: displayNameForToast)
                    } catch {
                        owsFailDebug("Failed to hide recipient")
                    }
                }
            }

        // Delete
        let saveRequest = CNSaveRequest()
        saveRequest.delete(contactToDelete)
        do {
            try contactStore.execute(saveRequest)
        } catch {
            didFail = true
            Logger.error("Failed to delete CNContact!")
            showGenericErrorToastAndDismiss()
        }

        spinnerContainer.isHidden = false
        spinnerView.startAnimating()
        self.view.isUserInteractionEnabled = false
    }

    private func showGenericErrorToastAndDismiss() {
        ToastController(text: CommonStrings.somethingWentWrongError).presentToastView(
            from: .bottom,
            of: self.viewControllerPresentingToast.view,
            inset: self.view.safeAreaInsets.bottom + Constants.toastInset
        )
        self.dismiss(animated: true)
    }

    /// Displays a toast confirming that a contact was
    /// successfully deleted.
    private func displayDeletedContactToast(displayNameForToast: String) {
        let toastMessage = String(
            format: OWSLocalizedString(
                "DELETE_CONTACT_CONFIRMATION_TOAST",
                comment: "Toast message confirming the system contact was deleted. Embeds {{The name of the user who was deleted.}}."
            ),
            displayNameForToast
        )
        ToastController(text: toastMessage).presentToastView(
            from: .bottom,
            of: self.viewControllerPresentingToast.view,
            inset: self.view.safeAreaInsets.bottom + Constants.toastInset
        )
    }
}

private class ContactDeletionAvatarImageView: UIImageView {
    override public func layoutSubviews() {
        super.layoutSubviews()
        self.clipsToBounds = true
        layer.cornerRadius = self.frame.size.width / 2
    }
}
