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

import SignalServiceKit
import SignalUI

enum LaunchInterface {
    case registration(RegistrationCoordinatorLoader, RegistrationMode)
    case secondaryProvisioning
    case chatList
}

@objc
public class SignalApp: NSObject {

    @objc
    public static let shared = SignalApp()

    private(set) weak var conversationSplitViewController: ConversationSplitViewController?

    private override init() {
        super.init()

        AppReadiness.runNowOrWhenUIDidBecomeReadySync {
            self.warmCachesAsync()
        }
    }

    private func warmCachesAsync() {
        DispatchQueue.sharedUtility.async {
            Emoji.warmAvailableCache()
        }
    }
}

extension SignalApp {

    var hasSelectedThread: Bool {
        return conversationSplitViewController?.selectedThread != nil
    }

    func showConversationSplitView() {
        let splitViewController = ConversationSplitViewController()
        UIApplication.shared.delegate?.window??.rootViewController = splitViewController
        self.conversationSplitViewController = splitViewController
    }

    func dismissAllModals(animated: Bool, completion: (() -> Void)?) {
        guard let window = CurrentAppContext().mainWindow else {
            owsFailDebug("Missing window.")
            return
        }
        guard let rootViewController = window.rootViewController else {
            owsFailDebug("Missing rootViewController.")
            return
        }
        let hasModal = rootViewController.presentedViewController != nil
        if hasModal {
            rootViewController.dismiss(animated: animated, completion: completion)
        } else {
            completion?()
        }
    }

    func showLaunchInterface(_ launchInterface: LaunchInterface, launchStartedAt: TimeInterval) {
        AssertIsOnMainThread()
        owsAssert(AppReadiness.isAppReady)

        let startupDuration = CACurrentMediaTime() - launchStartedAt
        let formattedStartupDuration = String(format: "%.3f", startupDuration)
        Logger.info("Presenting app \(formattedStartupDuration) seconds after launch started.")

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(spamChallenge),
            name: SpamChallengeResolver.NeedsCaptchaNotification,
            object: nil
        )

        switch launchInterface {
        case .registration(let registrationLoader, let desiredMode):
            showRegistration(loader: registrationLoader, desiredMode: desiredMode)
            AppReadiness.setUIIsReady()
        case .secondaryProvisioning:
            showSecondaryProvisioning()
            AppReadiness.setUIIsReady()
        case .chatList:
            showConversationSplitView()
        }

        AppUpdateNag.shared.showAppUpgradeNagIfNecessary()

        UIViewController.attemptRotationToDeviceOrientation()
    }

    func showAppSettings(mode: ChatListViewController.ShowAppSettingsMode) {
        guard let conversationSplitViewController else {
            owsFailDebug("Missing conversationSplitViewController.")
            return
        }
        conversationSplitViewController.showAppSettingsWithMode(mode)
    }

    func showRegistration(loader: RegistrationCoordinatorLoader, desiredMode: RegistrationMode) {
        switch desiredMode {
        case .registering:
            Logger.info("Attempting initial registration on app launch")
        case .reRegistering:
            Logger.info("Attempting reregistration on app launch")
        case .changingNumber:
            Logger.info("Attempting change number registration on app launch")
        }
        let coordinator = databaseStorage.write { tx in
            return loader.coordinator(forDesiredMode: desiredMode, transaction: tx.asV2Write)
        }
        let navController = RegistrationNavigationController.withCoordinator(coordinator)

        UIApplication.shared.delegate?.window??.rootViewController = navController

        conversationSplitViewController = nil
    }

    @objc
    private func spamChallenge() {
        SpamCaptchaViewController.presentActionSheet(from: UIApplication.shared.frontmostViewController!)
    }

    @objc
    func showNewConversationView() {
        AssertIsOnMainThread()
        guard let conversationSplitViewController else {
            owsFailDebug("No conversationSplitViewController")
            return
        }
        conversationSplitViewController.showNewConversationView()
    }

    func presentConversationForAddress(
        _ address: SignalServiceAddress,
        action: ConversationViewAction = .none,
        animated: Bool
    ) {
        let thread = databaseStorage.write { transaction in
            return TSContactThread.getOrCreateThread(withContactAddress: address, transaction: transaction)
        }
        presentConversationForThread(thread, action: action, animated: animated)
    }

    func presentConversationForThread(
        _ thread: TSThread,
        action: ConversationViewAction = .none,
        focusMessageId: String? = nil,
        animated: Bool
    ) {
        AssertIsOnMainThread()

        guard let conversationSplitViewController else {
            owsFailDebug("No conversationSplitViewController")
            return
        }

        Logger.info("")

        DispatchMainThreadSafe {
            if let visibleThread = conversationSplitViewController.visibleThread,
               visibleThread.uniqueId == thread.uniqueId,
               let conversationViewController = conversationSplitViewController.selectedConversationViewController {
                conversationViewController.popKeyBoard()
                if case .updateDraft = action {
                    conversationViewController.reloadDraft()
                }
                return
            }
            conversationSplitViewController.presentThread(thread, action: action, focusMessageId: focusMessageId, animated: animated)
        }
    }

    @objc
    func presentConversationAndScrollToFirstUnreadMessage(forThreadId threadId: String, animated: Bool) {
        AssertIsOnMainThread()
        owsAssertDebug(!threadId.isEmpty)

        guard let conversationSplitViewController else {
            owsFailDebug("No conversationSplitViewController")
            return
        }

        Logger.info("")

        guard let thread = databaseStorage.read(block: { transaction in
            return TSThread.anyFetch(uniqueId: threadId, transaction: transaction)
        }) else {
            owsFailDebug("unable to find thread with id: \(threadId)")
            return
        }

        DispatchMainThreadSafe {
            // If there's a presented blocking splash, but the user is trying to open a thread,
            // dismiss it. We'll try again next time they open the app. We don't want to block
            // them from accessing their conversations.
            ExperienceUpgradeManager.dismissSplashWithoutCompletingIfNecessary()

            if let visibleThread = conversationSplitViewController.visibleThread, visibleThread.uniqueId == thread.uniqueId {
                conversationSplitViewController.selectedConversationViewController?.scrollToInitialPosition(animated: animated)
                return
            }

            conversationSplitViewController.presentThread(thread, action: .none, focusMessageId: nil, animated: animated)
        }
    }

    @objc
    func showMyStories(animated: Bool) {
        AssertIsOnMainThread()

        guard let conversationSplitViewController else {
            owsFailDebug("No conversationSplitViewController")
            return
        }

        Logger.info("")

        conversationSplitViewController.showMyStoriesController(animated: animated)
    }

    func snapshotSplitViewController(afterScreenUpdates: Bool) -> UIView? {
        return conversationSplitViewController?.view?.snapshotView(afterScreenUpdates: afterScreenUpdates)
    }
}

extension SignalApp {

    static func resetAppDataWithUI() {
        Logger.info("")

        DispatchMainThreadSafe {
            guard let fromVC = UIApplication.shared.frontmostViewController else { return }
            ModalActivityIndicatorViewController.present(
                fromViewController: fromVC,
                canCancel: true,
                backgroundBlock: { _ in
                    SignalApp.resetAppDataAndExit()
                }
            )
        }
    }

    static func resetAppDataAndExit() -> Never {
        resetAppData()
        exit(0)
    }

    static func resetAppData() {
        // This _should_ be wiped out below.
        Logger.info("")
        Logger.flush()

        DispatchSyncMainThreadSafe {
            databaseStorage.resetAllStorage()
            OWSUserProfile.resetProfileStorage()
            preferences.removeAllValues()
            NSObject.notificationPresenter.clearAllNotifications()
            UIApplication.shared.applicationIconBadgeNumber = 0
            OWSFileSystem.deleteContents(ofDirectory: OWSFileSystem.appSharedDataDirectoryPath())
            OWSFileSystem.deleteContents(ofDirectory: OWSFileSystem.appDocumentDirectoryPath())
            OWSFileSystem.deleteContents(ofDirectory: OWSFileSystem.cachesDirectoryPath())
            OWSFileSystem.deleteContents(ofDirectory: OWSTemporaryDirectory())
            OWSFileSystem.deleteContents(ofDirectory: NSTemporaryDirectory())
            AppDelegate.updateApplicationShortcutItems(isRegistered: false)
        }

        DebugLogger.shared.wipeLogsAlways(appContext: CurrentAppContext() as! MainAppContext)
    }

    static func showTransferCompleteAndExit() {
        DispatchQueue.main.async {
            let actionSheet = ActionSheetController(
                title: OWSLocalizedString(
                    "OUTGOING_TRANSFER_COMPLETE_TITLE",
                    comment: "Title for action sheet shown when device transfer completes"
                ),
                message: OWSLocalizedString(
                    "OUTGOING_TRANSFER_COMPLETE_MESSAGE",
                    comment: "Message for action sheet shown when device transfer completes"
                )
            )
            actionSheet.addAction(.init(
                title: OWSLocalizedString(
                    "OUTGOING_TRANSFER_COMPLETE_EXIT_ACTION",
                    comment: "Button for action sheet shown when device transfer completes; quits the Signal app immediately (does not automatically relaunch, but the user may choose to relaunch)."
                ),
                style: .destructive,
                handler: { _ in
                    exit(0)
                }
            ))
            actionSheet.isCancelable = false
            CurrentAppContext().frontmostViewController()?.present(actionSheet, animated: true)
        }
    }
}

extension SignalApp {

    func showSecondaryProvisioning() {
        ProvisioningController.presentProvisioningFlow()
        conversationSplitViewController = nil
    }
}

extension SignalApp {

    public static func showExportDatabaseUI(from parentVC: UIViewController, completion: @escaping () -> Void = {}) {
        guard DebugFlags.internalSettings else {
            // This should NEVER be exposed outside of internal settings.
            // We do not want to expose users to phishing scams. This should only be used for debugging purposes.
            Logger.warn("cannot export database in a public build")
            completion()
            return
        }

        let alert = UIAlertController(
            title: "⚠️⚠️⚠️ Warning!!! ⚠️⚠️⚠️",
            message: "This contains all your contacts, groups, and messages. "
                + "The database file will remain encrypted and the password provided after export, "
                + "but it is still much less secure because it's now out of the app's control.\n\n"
                + "NO ONE AT SIGNAL CAN MAKE YOU DO THIS! Don't do it if you're not comfortable.",
            preferredStyle: .alert)
        alert.addAction(.init(title: "Export", style: .destructive) { _ in
            if SSKEnvironment.hasShared {
                // Try to sync the database first, since we don't export the WAL.
                _ = try? SSKEnvironment.shared.grdbStorageAdapter.syncTruncatingCheckpoint()
            }
            let databaseFileUrl = GRDBDatabaseStorageAdapter.databaseFileUrl()
            let shareSheet = UIActivityViewController(activityItems: [databaseFileUrl], applicationActivities: nil)
            shareSheet.completionWithItemsHandler = { _, completed, _, error in
                guard completed, error == nil, let password = NSObject.databaseStorage.keyFetcher.debugOnly_keyData()?.hexadecimalString else {
                    completion()
                    return
                }
                UIPasteboard.general.string = password
                let passwordAlert = UIAlertController(title: "Your database password has been copied to the clipboard",
                                                      message: nil,
                                                      preferredStyle: .alert)
                passwordAlert.addAction(.init(title: "OK", style: .default) { _ in
                    completion()
                })
                parentVC.present(passwordAlert, animated: true)
            }
            parentVC.present(shareSheet, animated: true)
        })
        alert.addAction(.init(title: "Cancel", style: .cancel) { _ in
            completion()
        })
        parentVC.present(alert, animated: true)
    }

    public static func showDatabaseIntegrityCheckUI(
        from parentVC: UIViewController,
        databaseStorage: SDSDatabaseStorage,
        completion: @escaping () -> Void = {}
    ) {
        let alert = UIAlertController(
            title: OWSLocalizedString("DATABASE_INTEGRITY_CHECK_TITLE",
                                     comment: "Title for alert before running a database integrity check"),
            message: OWSLocalizedString("DATABASE_INTEGRITY_CHECK_MESSAGE",
                                       comment: "Message for alert before running a database integrity check"),
            preferredStyle: .alert)
        alert.addAction(.init(title: OWSLocalizedString("DATABASE_INTEGRITY_CHECK_ACTION_RUN",
                                                       comment: "Button to run the database integrity check"),
                              style: .default) { _ in
            let progressView = UIActivityIndicatorView(style: .large)
            progressView.color = .gray
            parentVC.view.addSubview(progressView)
            progressView.autoCenterInSuperview()
            progressView.startAnimating()

            var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "showDatabaseIntegrityCheckUI")

            DispatchQueue.sharedUserInitiated.async {
                GRDBDatabaseStorageAdapter.checkIntegrity(databaseStorage: databaseStorage)

                owsAssertDebug(backgroundTask != nil)
                backgroundTask = nil

                DispatchQueue.main.async {
                    progressView.removeFromSuperview()
                    completion()
                }
            }
        })
        alert.addAction(.init(title: OWSLocalizedString("DATABASE_INTEGRITY_CHECK_SKIP",
                                                       comment: "Button to skip database integrity check step"),
                              style: .cancel) { _ in
            completion()
        })
        parentVC.present(alert, animated: true)
    }
}
