//
//  PreviewViewControllerProvider.swift
//  Tusker
//
//  Created by Shadowfacts on 10/10/18.
//  Copyright © 2018 Shadowfacts. All rights reserved.
//

import UIKit
import SafariServices
import Pachyderm
import WebURLFoundationExtras
import SwiftUI

@MainActor
protocol MenuActionProvider: AnyObject {
    var navigationDelegate: TuskerNavigationDelegate? { get }
    var toastableViewController: ToastableViewController? { get }
}

protocol MenuPreviewProvider: AnyObject {
    typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
    
    func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders?
}

protocol CustomPreviewPresenting {
    func presentFromPreview(presenter: UIViewController)
}

extension MenuActionProvider where Self: TuskerNavigationDelegate {
    var navigationDelegate: TuskerNavigationDelegate? { self }
}

extension MenuActionProvider where Self: ToastableViewController {
    var toastableViewController: ToastableViewController? { self }
}

extension MenuActionProvider {
    
    private var mastodonController: MastodonController? { navigationDelegate?.apiController }
    
    func actionsForProfile(accountID: String, source: PopoverSource, fetchRelationship: Bool = true) -> [UIMenuElement] {
        guard let mastodonController = mastodonController,
            let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
        
        var shareSection = [
            openInSafariAction(url: account.url),
            createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
                guard let self = self else { return }
                self.navigationDelegate?.showMoreOptions(forAccount: accountID, source: source)
            })
        ]
        
        guard let loggedInAccountID = mastodonController.accountInfo?.id else {
            // user is logged out
            return shareSection
        }
        
        var actionsSection: [UIMenuElement] = [
            createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { [weak self] (_) in
                guard let self = self else { return }
                let draft = self.mastodonController!.createDraft(mentioningAcct: account.acct)
                draft.visibility = .direct
                self.navigationDelegate?.compose(editing: draft)
            })
        ]
        var suppressSection: [UIMenuElement] = []
        
        if let ownAccount = mastodonController.account,
           accountID != ownAccount.id {
            actionsSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.followAction(for: $0, mastodonController: $1) }))
            actionsSection.append(UIDeferredMenuElement.uncached({ elementHandler in
                var listActions = mastodonController.lists.map { list in
                    UIAction(title: list.title, image: UIImage(systemName: "list.bullet")) { [unowned self] _ in
                        let req = List.add(list.id, accounts: [accountID])
                        mastodonController.run(req) { response in
                            if case .failure(let error) = response {
                                self.handleError(error, title: "Error Adding to List")
                            }
                        }
                    }
                }
                listActions.append(UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
                    Task { @MainActor in
                        let service = CreateListService(mastodonController: mastodonController, present: { [unowned self] in
                            self.navigationDelegate!.present($0, animated: true)
                        }) { list in
                            let req = List.add(list.id, accounts: [accountID])
                            let response = await mastodonController.runResponse(req)
                            if case .failure(let error) = response {
                                self.handleError(error, title: "Error Adding to List")
                            }
                        }
                        service.run()
                    }
                }))
                elementHandler([UIMenu(title: "Add to List", image: UIImage(systemName: "list.bullet"), children: listActions)])
            }))
            suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.hideReblogsAction(for: $0, mastodonController: $1) }))
            suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.muteAction(for: $0, mastodonController: $1) }))
            suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.blockAction(for: $0, mastodonController: $1) }))
            suppressSection.append(createAction(identifier: "report", title: "Report", systemImageName: "flag", handler: { [unowned self] _ in
                let view = ReportView(report: EditedReport(accountID: accountID), mastodonController: mastodonController)
                let host = UIHostingController(rootView: view)
                self.navigationDelegate?.present(host, animated: true)
            }))
        }
        
        addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID))
        
        return [
            UIMenu(options: .displayInline, children: shareSection),
            UIMenu(options: .displayInline, children: actionsSection),
            UIMenu(options: .displayInline, children: suppressSection),
        ]
    }
    
    func actionsForURL(_ url: URL, source: PopoverSource) -> [UIAction] {
        return [
            openInSafariAction(url: url),
            createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
                guard let self = self else { return }
                self.navigationDelegate?.showMoreOptions(forURL: url, source: source)
            })
        ]
    }
    
    func actionsForHashtag(_ hashtag: Hashtag, source: PopoverSource) -> [UIMenuElement] {
        var actionsSection: [UIMenuElement] = []
        if let mastodonController = mastodonController,
           mastodonController.loggedIn {
            let name = hashtag.name.lowercased()
            let context = mastodonController.persistentContainer.viewContext
            let existing = try? context.fetch(SavedHashtag.fetchRequest(name: name, account: mastodonController.accountInfo!)).first
            let saveSubtitle = "Shown in the Explore section of Tusker"
            let saveImage = UIImage(systemName: existing != nil ? "minus" : "plus")
            actionsSection = [
                UIAction(title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", subtitle: saveSubtitle, image: saveImage, handler: { (_) in
                    if let existing = existing {
                        context.delete(existing)
                    } else {
                        _ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
                    }
                    mastodonController.persistentContainer.save(context: context)
                })
            ]
            if mastodonController.instanceFeatures.canFollowHashtags {
                let existing = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == name })
                let subtitle = "Posts appear in your Home timeline"
                let image = UIImage(systemName: existing != nil ? "person.badge.minus" : "person.badge.plus")
                actionsSection.append(UIAction(title: existing != nil ? "Unfollow" : "Follow", subtitle: subtitle, image: image) { [unowned self] _ in
                    Task {
                        await ToggleFollowHashtagService(hashtagName: hashtag.name, presenter: navigationDelegate!).toggleFollow()
                    }
                })
            }
        }
        
        let shareSection: [UIMenuElement]
        if let url = URL(hashtag.url) {
            shareSection = actionsForURL(url, source: source)
        } else {
            shareSection = []
        }
        
        return [
            UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
            UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
        ]
    }
    
    func actionsForStatus(_ status: StatusMO, source: PopoverSource, includeStatusButtonActions: Bool = true) -> [UIMenuElement] {
        guard let mastodonController = mastodonController else { return [] }
        
        guard let accountID = mastodonController.accountInfo?.id,
              let account = mastodonController.account else {
            // user is logged out
            // i don't know why exactly a status wouldn't have a url, but apparently it happens, so:
            if let url = status.url {
                return [
                    openInSafariAction(url: url),
                    createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
                        guard let self = self else { return }
                        self.navigationDelegate?.showMoreOptions(forStatus: status.id, source: source)
                    }),
                ]
            } else {
                return []
            }
        }
        let bookmarked = status.bookmarked ?? false
        
        var toggleableSection = [
            createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
                guard let self = self else { return }
                let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
                self.mastodonController?.run(request) { (response) in
                    switch response {
                    case .success(let status, _):
                        self.mastodonController?.persistentContainer.addOrUpdate(status: status)
                        
                    case .failure(let error):
                        self.handleError(error, title: "Error \(bookmarked ? "Unb" : "B")ookmarking")
                    }
                }
            }),
        ]
        
        if #available(iOS 16.0, *),
           includeStatusButtonActions {
            let favorited = status.favourited
            // TODO: move this color into an asset catalog or something
            var favImage = UIImage(systemName: favorited ? "star.fill" : "star")!
            if favorited {
                favImage = favImage.withTintColor(UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1), renderingMode: .alwaysOriginal)
            }
            toggleableSection.insert(createAction(identifier: "favorite", title: favorited ? "Unfavorite" : "Favorite", image: favImage, handler: { [weak self] _ in
                guard let self,
                      let navigationDelegate = self.navigationDelegate else { return }
                Task { @MainActor in
                    await FavoriteService(status: status, mastodonController: mastodonController, presenter: navigationDelegate).toggleFavorite()
                }
            }), at: 0)
            
            let reblogged = status.reblogged
            var reblogImage = UIImage(systemName: "repeat")!
            if reblogged {
                reblogImage = reblogImage.withTintColor(UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1), renderingMode: .alwaysOriginal)
            }
            toggleableSection.insert(createAction(identifier: "reblog", title: reblogged ? "Unreblog" : "Reblog", image: reblogImage, handler: { [weak self] _ in
                guard let self,
                      let navigationDelegate = self.navigationDelegate else { return }
                Task { @MainActor in
                    await ReblogService(status: status, mastodonController: mastodonController, presenter: navigationDelegate).toggleReblog()
                }
            }), at: 1)
        }
        
        var actionsSection: [UIMenuElement] = []
        
        if includeStatusButtonActions {
            actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
                guard let self = self else { return }
                self.navigationDelegate?.compose(inReplyToID: status.id)
            }), at: 0)
        }
        
        // only allow muting conversations that either current user posted or is participating in (technically, is mentioned, since that's the best we can do)
        if status.account.id == account.id || status.mentions.contains(where: { $0.id == account.id }) {
            let muted = status.muted
            toggleableSection.append(createAction(identifier: "mute", title: muted ? "Unmute Conversation" : "Mute Conversation", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
                guard let self = self else { return }
                let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
                self.mastodonController?.run(request) { (response) in
                    switch response {
                    case .success(let status, _):
                        self.mastodonController?.persistentContainer.addOrUpdate(status: status)
                    case .failure(let error):
                        self.handleError(error, title: "Error \(muted ? "Unm" : "M")uting")
                    }
                }
            }))
        }
        
        if status.poll != nil {
            actionsSection.append(createAction(identifier: "refresh", title: "Refresh Poll", systemImageName: "arrow.clockwise", handler: { [weak self] (_) in
                guard let mastodonController = self?.mastodonController else { return }
                let request = Client.getStatus(id: status.id)
                mastodonController.run(request, completion: { (response) in
                    switch response {
                    case .success(let status, _):
                        // todo: this shouldn't really use the viewContext, but for some reason saving the
                        // backgroundContext with the new version of the status isn't updating the viewContext
                        DispatchQueue.main.async {
                            mastodonController.persistentContainer.addOrUpdate(status: status, context: mastodonController.persistentContainer.viewContext)
                        }
                    case .failure(let error):
                        self?.handleError(error, title: "Error Refreshing Poll")
                    }
                })
            }))
        }
        
        if account.id == status.account.id {
            if mastodonController.instanceFeatures.profilePinnedStatuses {
                let pinned = status.pinned ?? false
                toggleableSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
                    guard let self = self else { return }
                    let request = (pinned ? Status.unpin : Status.pin)(status.id)
                    self.mastodonController?.run(request, completion: { [weak self] (response) in
                        guard let self = self else { return }
                        switch response {
                        case .success(let status, _):
                            self.mastodonController?.persistentContainer.addOrUpdate(status: status)
                        case .failure(let error):
                            self.handleError(error, title: "Error \(pinned ? "Unp" :"P")inning")
                        }
                    })
                }))
            }
            
            if mastodonController.instanceFeatures.editStatuses {
                actionsSection.append(UIAction(title: "Edit Post", image: UIImage(systemName: "pencil"), handler: { [weak self] _ in
                    guard let navigationDelegate = self?.navigationDelegate else {
                        return
                    }
                    @MainActor func doEdit() async {
                        let source: StatusSource
                        do {
                            source = try await FetchStatusSourceService(statusID: status.id, delegate: navigationDelegate).run()
                        } catch let error as Client.Error {
                            self?.handleError(error, title: "Error Fetching Source")
                            return
                        } catch {
                            return
                        }
                        let draft = navigationDelegate.apiController.createDraft(editing: status, source: source)
                        navigationDelegate.compose(editing: draft)
                    }
                    if status.poll != nil {
                        let alert = UIAlertController(title: "Edit Post with Poll?", message: "This will remove any votes that have already been placed.", preferredStyle: .alert)
                        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
                        alert.addAction(UIAlertAction(title: "Edit", style: .default, handler: { _ in
                            Task {
                                await doEdit()
                            }
                        }))
                        navigationDelegate.present(alert, animated: true)
                    } else {
                        Task {
                            await doEdit()
                        }
                    }
                }))
            }
            
            actionsSection.append(UIMenu(title: "Delete Post", image: UIImage(systemName: "trash"), children: [
                UIAction(title: "Cancel", handler: { _ in }),
                UIAction(title: "Delete Post", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [weak self] _ in
                    guard let self,
                          let navigationDelegate = self.navigationDelegate else {
                        return
                    }
                    Task { @MainActor in
                        let service = DeleteStatusService(status: status, mastodonController: mastodonController, presenter: navigationDelegate)
                        await service.run()
                    }
                })
            ]))
        } else {
            actionsSection.append(createAction(identifier: "report", title: "Report", systemImageName: "flag", handler: { [weak self] _ in
                let report = EditedReport(accountID: status.account.id)
                report.statusIDs = [status.id]
                let view = ReportView(report: report, mastodonController: mastodonController)
                let host = UIHostingController(rootView: view)
                self?.navigationDelegate?.present(host, animated: true)
            }))
        }
        
        var shareSection: [UIAction] = []
        if let url = status.url {
            shareSection.append(openInSafariAction(url: url))
        } else {
            Logging.general.fault("Status missing URL: id=\(status.id, privacy: .public), reblog=\((status.reblog?.id).debugDescription, privacy: .public)")
        }
        shareSection.append(createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
            guard let self = self else { return }
            self.navigationDelegate?.showMoreOptions(forStatus: status.id, source: source)
        }))
        
        addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
        
        if #available(iOS 16.0, *) {
            let toggleableAndActions = toggleableSection + actionsSection
            return [
                UIMenu(options: .displayInline, preferredElementSize: toggleableAndActions.count == 1 ? .large : .medium, children: toggleableAndActions),
                UIMenu(options: .displayInline, children: shareSection),
            ]
        } else {
            return [
                UIMenu(options: .displayInline, children: shareSection),
                UIMenu(options: .displayInline, children: toggleableSection),
                UIMenu(options: .displayInline, children: actionsSection),
            ]
        }
    }
    
    func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {
        guard let url = URL(card.url) else {
            return []
        }
        return [
            openInSafariAction(url: url),
            createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
                guard let self else { return }
                self.navigationDelegate?.showMoreOptions(forURL: url, source: source)
            }),
            createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in
                guard let self = self else { return }
                
                var text = ""
                let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
                if !title.isEmpty {
                    text += title
                    text += ":\n"
                }
                text += url.absoluteString
                
                let draft = self.mastodonController!.createDraft(text: text)
                self.navigationDelegate?.compose(editing: draft)
            })
        ]
    }

    private func createAction(identifier: String, title: String, systemImageName: String?, handler: @escaping (UIAction) -> Void) -> UIAction {
        let image: UIImage?
        if let name = systemImageName {
            image = UIImage(systemName: name)
        } else {
            image = nil
        }
        return createAction(identifier: identifier, title: title, image: image, handler: handler)
    }
    
    private func createAction(identifier: String, title: String, image: UIImage?, handler: @escaping UIActionHandler) -> UIAction {
        return UIAction(title: title, image: image, identifier: UIAction.Identifier(identifier), discoverabilityTitle: nil, attributes: [], state: .off, handler: handler)
    }
    
    private func openInSafariAction(url: URL) -> UIAction {
        return createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { [weak self] (_) in
            self?.navigationDelegate?.selected(url: url, allowResolveStatuses: false, allowUniversalLinks: false)
        })
    }
    
    private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
        let options = UIWindowScene.ActivationRequestOptions()
        options.preferredPresentationStyle = .automatic
        actions.append(UIWindowScene.ActivationAction { (_) in
            let activity = activity()
            activity.displaysAuxiliaryScene = true
            return .init(userActivity: activity, options: options, preview: nil)
        })
    }
    
    private func handleError(_ error: Client.Error, title: String) {
        if let navigationDelegate {
            let config = ToastConfiguration(from: error, with: title, in: navigationDelegate, retryAction: nil)
            DispatchQueue.main.async {
                navigationDelegate.showToast(configuration: config, animated: true)
            }
        }
    }
    
    private func handleSuccess(title: String) {
        if let toastable = self.toastableViewController {
            var config = ToastConfiguration(title: title)
            config.systemImageName = "checkmark"
            config.dismissAutomaticallyAfter = 2
            DispatchQueue.main.async {
                toastable.showToast(configuration: config, animated: true)
            }
        }
    }
    
    private func relationshipAction(_ fetch: Bool, accountID: String, mastodonController: MastodonController, builder: @escaping @MainActor (RelationshipMO, MastodonController) -> UIMenuElement?) -> UIDeferredMenuElement {
        return UIDeferredMenuElement.uncached({ @MainActor elementHandler in
            // workaround for #198, may result in showing outdated relationship, so only do so where necessary
            if !fetch || ProcessInfo.processInfo.isiOSAppOnMac,
               let mo = mastodonController.persistentContainer.relationship(forAccount: accountID) {
                if let action = builder(mo, mastodonController) {
                    elementHandler([action])
                } else {
                    elementHandler([])
                }
            } else {
                let relationship = Task {
                    await fetchRelationship(accountID: accountID, mastodonController: mastodonController)
                }
                Task { @MainActor in
                    if let relationship = await relationship.value,
                       let action = builder(relationship, mastodonController) {
                        elementHandler([action])
                    } else {
                        elementHandler([])
                    }
                }
            }
        })
    }
    
    @MainActor
    private func followAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement {
        let accountID = relationship.accountID
        let following = relationship.following
        let requested = relationship.requested
        let title = following ? "Unfollow" : requested ? "Cancel Request" : "Follow"
        let imageName = following || requested  ? "person.badge.minus" : "person.badge.plus"
        return createAction(identifier: "follow", title: title, systemImageName: imageName) { [weak self] _ in
            let request = (following || requested ? Account.unfollow : Account.follow)(accountID)
            mastodonController.run(request) { response in
                switch response {
                case .failure(let error):
                    self?.handleError(error, title: following ? "Error Unfollowing" : requested ? "Error Cancelinng Request" : "Error Following")
                case .success(let relationship, _):
                    mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
                    if requested { // was requested, now cancelled
                        self?.handleSuccess(title: "Follow Request Cancelled")
                    } else if following { // was following, now unfollowed
                        self?.handleSuccess(title: "Unfollowed")
                    } else if relationship.followRequested { // was not following, now requested
                        self?.handleSuccess(title: "Request Sent")
                    } else { // was not following, not now requested, assume success
                        self?.handleSuccess(title: "Followed")
                    }
                }
            }
        }
    }
    
    @MainActor
    private func blockAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement {
        let accountID = relationship.accountID
        let displayName = relationship.account!.displayOrUserName
        let host = relationship.account!.url.host!
        let handler = { (block: Bool) in
            return { [weak self] (_: UIAction) in
                let req = block ? Account.block(accountID) : Account.unblock(accountID)
                _ = mastodonController.run(req) { response in
                    switch response {
                    case .failure(let error):
                        self?.handleError(error, title: "Error \(block ? "B" : "Unb")locking")
                    case .success(let relationship, _):
                        mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
                        self?.handleSuccess(title: "\(block ? "B" : "Unb")locked")
                    }
                }
            }
        }
        let domainHandler = { (block: Bool) in
            return { [weak self] (_: UIAction) in
                let req = block ? Client.block(domain: host) : Client.unblock(domain: host)
                mastodonController.run(req) { response in
                    switch response {
                    case .failure(let error):
                        self?.handleError(error, title: "Error \(block ? "B" : "Unb")locking")
                    case .success(_, _):
                        self?.handleSuccess(title: "Domain \(block ? "B" : "Unb")locked")
                    }
                }
            }
        }
        if relationship.domainBlocking {
            return createAction(identifier: "block", title: "Unblock \(host)", systemImageName: "circle.slash", handler: domainHandler(false))
        } else if relationship.blocking {
            return createAction(identifier: "block", title: "Unblock \(displayName)", systemImageName: "circle.slash", handler: handler(false))
        } else {
            let image = UIImage(systemName: "circle.slash")
            return UIMenu(title: "Block", image: image, children: [
                UIAction(title: "Cancel", handler: { _ in }),
                UIAction(title: "Block \(displayName)", image: image, attributes: .destructive, handler: handler(true)),
                UIAction(title: "Block \(host)", image: image, attributes: .destructive, handler: domainHandler(true))
            ])
        }
    }
    
    @MainActor
    private func muteAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement {
        if relationship.muting || relationship.mutingNotifications {
            return UIAction(title: "Unmute", image: UIImage(systemName: "speaker")) { [weak self] _ in
                let req = Account.unmute(relationship.accountID)
                mastodonController.run(req) { response in
                    switch response {
                    case .failure(let error):
                        self?.handleError(error, title: "Error Unmuting")
                    case .success(let relationship, _):
                        mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
                        self?.handleSuccess(title: "Unmuted")
                    }
                }
            }
        } else {
            return UIAction(title: "Mute", image: UIImage(systemName: "speaker.slash")) { [weak self] _ in
                let view = MuteAccountView(account: relationship.account!, mastodonController: mastodonController)
                let host = UIHostingController(rootView: view)
                self?.navigationDelegate?.present(host, animated: true)
            }
        }
    }
    
    @MainActor
    private func hideReblogsAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement? {
        // don't show action for people that the user isn't following and isn't already hiding reblogs for
        guard relationship.following || relationship.showingReblogs else {
            return nil
        }
        let title = relationship.showingReblogs ? "Hide Reblogs" : "Show Reblogs"
        // todo: need alternate repeat icon to use here
        return UIAction(title: title, image: nil) { [weak self] _ in
            let req = Account.setShowReblogs(relationship.accountID, showReblogs: !relationship.showingReblogs)
            mastodonController.run(req) { response in
                switch response {
                case .failure(let error):
                    self?.handleError(error, title: "Error \(relationship.showingReblogs ? "Hiding" : "Showing") Reblogs")
                case .success(let relationship, _):
                    mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
                    self?.handleSuccess(title: relationship.showingReblogs ? "Reblogs Shown" : "Reblogs Hidden")
                }
            }
        }
    }
    
}

private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> RelationshipMO? {
    let req = Client.getRelationships(accounts: [accountID])
    guard let (relationships, _) = try? await mastodonController.run(req),
          let r = relationships.first else {
        return nil
    }
    return await withCheckedContinuation { continuation in
        mastodonController.persistentContainer.addOrUpdate(relationship: r, in: mastodonController.persistentContainer.viewContext) { mo in
            continuation.resume(returning: mo)
        }
    }
}

struct MenuPreviewHelper {
    static func willPerformPreviewAction(animator: UIContextMenuInteractionCommitAnimating, presenter: UIViewController) {
        if let viewController = animator.previewViewController {
            animator.preferredCommitStyle = .pop
            animator.addCompletion {
                if let customPresenting = viewController as? CustomPreviewPresenting {
                    customPresenting.presentFromPreview(presenter: presenter)
                } else {
                    presenter.show(viewController, sender: nil)
                }
            }
        }
    }
}

extension LargeImageViewController: CustomPreviewPresenting {
    func presentFromPreview(presenter: UIViewController) {
        presenter.present(self, animated: true)
    }
}

extension GalleryViewController: CustomPreviewPresenting {
    func presentFromPreview(presenter: UIViewController) {
        presenter.present(self, animated: true)
    }
}

extension SFSafariViewController: CustomPreviewPresenting {
    func presentFromPreview(presenter: UIViewController) {
        presenter.present(self, animated: true)
    }
}
