//
//  AppDelegate.swift
//  ConfirmediOS
//
//  Copyright © 2018 Confirmed, Inc. All rights reserved.
//

import UIKit
import NetworkExtension
import SwiftyStoreKit
import KeychainAccess
import SafariServices
import SwiftMessages
import StoreKit
import CloudKit
import CocoaLumberjackSwift
import PopupDialog
import PromiseKit
import UserNotifications
import WidgetKit
import BackgroundTasks

let fileLogger: DDFileLogger = DDFileLogger()

let kHasShownTitlePage: String = "kHasShownTitlePage"

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var window: UIWindow?
    let noInternetMessageView = MessageView.viewFromNib(layout: .statusLine)
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // Clear local data for testing
//         try? keychain.removeAll()
//        for d in defaults.dictionaryRepresentation() {
//            defaults.removeObject(forKey: d.key)
//        }
//        return true

        // Set up basic logging
        setupLocalLogger()
        
        DDLogInfo("Creating protectionAccess.check file...")
        ProtectedFileAccess.createProtectionAccessCheckFile()
        
        UNUserNotificationCenter.current().delegate = self
        
        // Set up PopupDialog
        let dialogAppearance = PopupDialogDefaultView.appearance()
        if #available(iOS 13.0, *) {
            dialogAppearance.backgroundColor = .systemBackground
            dialogAppearance.titleColor = .label
            dialogAppearance.messageColor = .label
        } else {
            dialogAppearance.backgroundColor = .white
            dialogAppearance.titleColor = .black
            dialogAppearance.messageColor = .darkGray
        }
        dialogAppearance.titleFont            = fontBold15
        dialogAppearance.titleTextAlignment   = .center
        dialogAppearance.messageFont          = fontMedium15
        dialogAppearance.messageTextAlignment = .center
        let buttonAppearance = DefaultButton.appearance()
        if #available(iOS 13.0, *) {
            buttonAppearance.buttonColor = .systemBackground
            buttonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1)
        }
        else {
            buttonAppearance.buttonColor    = .clear
            buttonAppearance.separatorColor = UIColor(white: 0.9, alpha: 1)
        }
        buttonAppearance.titleFont      = fontSemiBold17
        buttonAppearance.titleColor     = UIColor.tunnelsBlue
        let dynamicButtonAppearance = DynamicButton.appearance()
        if #available(iOS 13.0, *) {
            dynamicButtonAppearance.buttonColor = .systemBackground
            dynamicButtonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1)
        }
        else {
            dynamicButtonAppearance.buttonColor    = .clear
            dynamicButtonAppearance.separatorColor = UIColor(white: 0.9, alpha: 1)
        }
        dynamicButtonAppearance.titleFont      = fontSemiBold17
        dynamicButtonAppearance.titleColor     = UIColor.tunnelsBlue
        let cancelButtonAppearance = CancelButton.appearance()
        if #available(iOS 13.0, *) {
            cancelButtonAppearance.buttonColor = .systemBackground
            cancelButtonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1)
        }
        else {
            cancelButtonAppearance.buttonColor    = .clear
            cancelButtonAppearance.separatorColor = UIColor(white: 0.9, alpha: 1)
        }
        cancelButtonAppearance.titleFont      = fontSemiBold17
        cancelButtonAppearance.titleColor     = UIColor.lightGray

        // Lockdown default lists
        setupFirewallDefaultBlockLists()
        
        // Whitelist default domains
        setupLockdownWhitelistedDomains()
        
        // Show indicator at top when internet not reachable
        reachability?.whenReachable = { reachability in
            SwiftMessages.hide()
        }
        reachability?.whenUnreachable = { _ in
            DDLogInfo("Internet not reachable")
            self.noInternetMessageView.backgroundView.backgroundColor = UIColor.orange
            self.noInternetMessageView.bodyLabel?.textColor = UIColor.white
            self.noInternetMessageView.configureContent(body: NSLocalizedString("No Internet Connection", comment: ""))
            var noInternetMessageViewConfig = SwiftMessages.defaultConfig
            noInternetMessageViewConfig.presentationContext = .window(windowLevel: UIWindow.Level(rawValue: 0))
            noInternetMessageViewConfig.preferredStatusBarStyle = .lightContent
            noInternetMessageViewConfig.duration = .forever
            SwiftMessages.show(config: noInternetMessageViewConfig, view: self.noInternetMessageView)
        }
        do {
            try reachability?.startNotifier()
        } catch {
            DDLogError("Unable to start reachability notifier")
        }
        
        // Content Blocker
        SFContentBlockerManager.reloadContentBlocker( withIdentifier: "com.confirmed.lockdown.Confirmed-Blocker") { (_ error: Error?) -> Void in
            if error != nil {
                DDLogError("Error loading Content Blocker: \(String(describing: error))")
            }
        }
        
        // Prepare IAP
        VPNSubscription.cacheLocalizedPrices()
        SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
            for purchase in purchases {
                DDLogInfo("LAUNCH: Processing Purchase\n\(purchase)");
                if purchase.transaction.transactionState == .purchased || purchase.transaction.transactionState == .restored {
                    if purchase.needsFinishTransaction {
                        DDLogInfo("Finishing transaction for purchase: \(purchase)")
                        SwiftyStoreKit.finishTransaction(purchase.transaction)
                    }
                }
            }
        }
        
        // Periodically check if the firewall is functioning correctly - every 2.5 hours
        if #available(iOS 13.0, *) {
            DDLogInfo("BGTask: Registering BGTask id \(FirewallRepair.identifier)")
            BGTaskScheduler.shared.register(forTaskWithIdentifier: FirewallRepair.identifier, using: nil) { task in
                DDLogInfo("BGTask: Task starting")
                FirewallRepair.handleAppRefresh(task)
            }
        }
        else {
            UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
        }

        // WORKAROUND: allows the widget to toggle VPN
        application.registerForRemoteNotifications()
        setupWidgetToggleWorkaround()
        
        // If not yet agreed to privacy policy, set initial view controller to TitleViewController
        if (defaults.bool(forKey: kHasShownTitlePage) == false) {
            // TODO: removed this check because this was causing crashes possibly due to Locale
            // don't show onboarding page for anyone who installed before Aug 16th
        //            let formatter = DateFormatter()
        //            formatter.dateFormat = "yyyy/MM/dd HH:mm"
        //            let tutorialCutoffDate = formatter.date(from: "2019/08/16 00:00")!.timeIntervalSince1970;
        //            if let appInstall = appInstallDate, appInstall.timeIntervalSince1970 < tutorialCutoffDate {
        //                print("Not showing onboarding page, installation epoch \(appInstall.timeIntervalSince1970)")
        //            }
        //            else {
                DDLogInfo("Showing onboarding page")
                self.window = UIWindow(frame: UIScreen.main.bounds)
                let storyboard = UIStoryboard(name: "Main", bundle: nil)
                let viewController = storyboard.instantiateViewController(withIdentifier: "titleViewController") as! TitleViewController
                self.window?.rootViewController = viewController
                self.window?.makeKeyAndVisible()
        //            }
        }
        
        return true
    }
    
    func applicationDidEnterBackground(_ application: UIApplication) {
        DDLogInfo("applicationDidEnterBackground")
        FirewallRepair.reschedule()
    }
    
    func applicationDidBecomeActive(_ application: UIApplication) {
        DDLogInfo("applicationDidBecomeActive")
        PacketTunnelProviderLogs.flush()
        updateMetrics(.resetIfNeeded, rescheduleNotifications: .always)
        
        FirewallRepair.run(context: .homeScreenDidLoad)
    }
    
    func applicationWillResignActive(_ application: UIApplication) {
        if #available(iOS 14.0, *) {
            WidgetCenter.shared.reloadAllTimelines()
        }
    }
    
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        DDLogError("Successfully registered for remote notification: \(deviceToken)")
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        DDLogError("Error registering for remote notification: \(error)")
    }
    
    func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        
        // Deprecated, uses BackgroundTasks after iOS 13+
        DDLogInfo("BGF called, running Repair")
        FirewallRepair.run(context: .backgroundRefresh) { (result) in
            switch result {
            case .failed:
                DDLogInfo("BGF: failed")
                completionHandler(.failed)
            case .repairAttempted:
                DDLogInfo("BGF: attempted")
                completionHandler(.newData)
            case .noAction:
                DDLogInfo("BGF: no action")
                completionHandler(.noData)
            }
        }
    }
    
    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return .portrait
    }

    // MARK: - WIDGET TOGGLE WORKAROUND
    func setupWidgetToggleWorkaround() {
        DDLogInfo("Setting up CloudKit Workaround")
        clearDatabaseForRecord(recordName: kOpenFirewallTunnelRecord)
        clearDatabaseForRecord(recordName: kCloseFirewallTunnelRecord)
        clearDatabaseForRecord(recordName: kRestartFirewallTunnelRecord)
        let privateDatabase = CKContainer(identifier: kICloudContainer).privateCloudDatabase
        privateDatabase.fetchAllSubscriptions(completionHandler: { subscriptions, error in
            // always set up cloudkit subscriptions - no downside to doing it
//            if error == nil, let subs = subscriptions {
////                for sub in subs {
////                    print("deleting sub: \(sub.subscriptionID)")
////                    privateDatabase.delete(withSubscriptionID: sub.subscriptionID, completionHandler: {
////                        result, error in
////                        print("result: \(result)")
////                    })
////                }
////                return
//                var isSubscribedToOpen = false
//                var isSubscribedToClose = false
//                var isSubscribedToRestart = false
//                for subscriptionObject in subs {
//                    if subscriptionObject.notificationInfo?.category == kCloseFirewallTunnelRecord {
//                        isSubscribedToClose = true
//                    }
//                    if subscriptionObject.notificationInfo?.category == kOpenFirewallTunnelRecord {
//                        isSubscribedToOpen = true
//                    }
//                    if subscriptionObject.notificationInfo?.category == kRestartFirewallTunnelRecord {
//                        isSubscribedToRestart = true
//                    }
//                }
//                if !isSubscribedToOpen {
//                    self.setupCloudKitSubscription(categoryName: kOpenFirewallTunnelRecord)
//                }
//                if !isSubscribedToClose {
//                    self.setupCloudKitSubscription(categoryName: kCloseFirewallTunnelRecord)
//                }
//                if !isSubscribedToRestart {
//                    self.setupCloudKitSubscription(categoryName: kRestartFirewallTunnelRecord)
//                }
//            }
//            else {
                self.setupCloudKitSubscription(categoryName: kCloseFirewallTunnelRecord)
                self.setupCloudKitSubscription(categoryName: kOpenFirewallTunnelRecord)
                self.setupCloudKitSubscription(categoryName: kRestartFirewallTunnelRecord)
//            }
        })
    }
    
    func setupCloudKitSubscription(categoryName: String) {
        let privateDatabase = CKContainer(identifier: kICloudContainer).privateCloudDatabase
        let subscription = CKQuerySubscription(recordType: categoryName,
                                               predicate: NSPredicate(value: true),
                                               options: .firesOnRecordCreation)
        let notificationInfo = CKSubscription.NotificationInfo()
        //notificationInfo.alertBody = "" // iOS 13 doesn't like this - fails to trigger notification
        notificationInfo.shouldSendContentAvailable = true
        notificationInfo.shouldBadge = false
        notificationInfo.category = categoryName
        subscription.notificationInfo = notificationInfo
        privateDatabase.save(subscription,
                             completionHandler: ({returnRecord, error in
                                if let err = error {
                                    DDLogInfo("Could not save CloudKit subscription (not signed in?) \(err)")
                                } else {
                                    DispatchQueue.main.async() {
                                        DDLogInfo("Successfully saved CloudKit subscription")
                                    }
                                }
                             }))
    }
    
    func clearDatabaseForRecord(recordName: String) {
        let privateDatabase = CKContainer(identifier: kICloudContainer).privateCloudDatabase
        let predicate = NSPredicate(value: true)
        let query = CKQuery(recordType: recordName, predicate: predicate)
        
        privateDatabase.perform(query, inZoneWith: nil) { (record, error) in
            if let err = error {
                DDLogError("Error querying for CKRecordType: \(recordName) - \(err)")
            }
            for aRecord in record! {
                privateDatabase.delete(withRecordID: aRecord.recordID, completionHandler: { (recordID, error) in
                    DDLogInfo("Deleting record \(aRecord.recordID)")
                })
            }
        }
    }
    
    func application(_ application: UIApplication,
                     didReceiveRemoteNotification userInfo: [AnyHashable : Any],
                     fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        DDLogInfo("Receiving remote notification")
        if let aps = userInfo["aps"] as? NSDictionary {
            if let message = aps["category"] as? NSString {
                if message.contains(kCloseFirewallTunnelRecord) {
                    FirewallController.shared.setEnabled(false, isUserExplicitToggle: true, completion: { _ in })
                }
                else if message.contains(kOpenFirewallTunnelRecord) {
                    FirewallController.shared.setEnabled(true, isUserExplicitToggle: true, completion: { _ in })
                }
                else if message.contains(kRestartFirewallTunnelRecord) {
                    FirewallController.shared.restart(completion: {
                        error in
                        if error != nil {
                            DDLogError("Error restarting firewall on RemoteNotification: \(error!)")
                        }
                    })
                }
                clearDatabaseForRecord(recordName: kOpenFirewallTunnelRecord)
                clearDatabaseForRecord(recordName: kCloseFirewallTunnelRecord)
                clearDatabaseForRecord(recordName: kRestartFirewallTunnelRecord)
            }
        }
        DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 10.0, execute: {
            completionHandler(.newData)
        })
    }
    
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {

        guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
            let host = components.host else {
                print("Invalid URL")
                return false
        }
        
        if (host == "resetsuccessful") {
            let popup = PopupDialog(title: "Password Reset Successfully",
                                    message: "Please Sign In With Your New Password",
                                    image: nil,
                                    buttonAlignment: .horizontal,
                                    transitionStyle: .bounceDown,
                                    preferredWidth: 270,
                                    tapGestureDismissal: true,
                                    panGestureDismissal: false,
                                    hideStatusBar: false,
                                    completion: nil)
            popup.addButtons([
                DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {
                    if let hvc = self.getCurrentViewController() as? HomeViewController {
                        AccountUI.presentSignInToAccount(on: hvc)
                    }
                }
            ])
            self.getCurrentViewController()?.present(popup, animated: true, completion: nil)
            return true
        }
        
        else if (host == "changeVPNregion") {
            if let home = self.getCurrentViewController() as? HomeViewController {
                home.showSetRegion(self)
            }
        }
        
        else if (host == "showMetrics") {
            if let home = self.getCurrentViewController() as? HomeViewController {
                home.showBlockLog(self)
            }
        }
        
        else if (host == "toggleFirewall") {
            if let home = self.getCurrentViewController() as? HomeViewController {
                home.toggleFirewall(self)
            }
        }
        
        else if (host == "toggleVPN") {
            if let home = self.getCurrentViewController() as? HomeViewController {
                home.toggleVPN(self)
            }
        }
        
        else if (host == "emailconfirmed") {
            // test the stored login
            guard let apiCredentials = getAPICredentials() else {
                let popup = PopupDialog(title: "Error",
                                        message: NSLocalizedString("No stored API credentials found. Please contact team@lockdownprivacy.com about this error.", comment: ""),
                                        image: nil,
                                        buttonAlignment: .horizontal,
                                        transitionStyle: .bounceDown,
                                        preferredWidth: 270,
                                        tapGestureDismissal: true,
                                        panGestureDismissal: false,
                                        hideStatusBar: false,
                                        completion: nil)
                popup.addButtons([
                   DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {}
                ])
                getCurrentViewController()?.present(popup, animated: true, completion: nil)
                return true
            }
            firstly {
                try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password)
            }
            .done { (signin: SignIn) in
                // successfully signed in with no errors, show confirmation success
                setAPICredentialsConfirmed(confirmed: true)
                // logged in and confirmed - update this email with the receipt and refresh VPN credentials
                firstly { () -> Promise<SubscriptionEvent> in
                    try Client.subscriptionEvent()
                }
                .then { (result: SubscriptionEvent) -> Promise<GetKey> in
                    try Client.getKey()
                }
                .done { (getKey: GetKey) in
                    try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64)
                    if (getUserWantsVPNEnabled() == true) {
                        VPNController.shared.restart()
                    }
                }
                .catch { error in
                    // it's okay for this to error out with "no subscription in receipt"
                    DDLogError("HomeViewController ConfirmEmail subscriptionevent error (ok for it to be \"no subscription in receipt\"): \(error)")
                }
                let popup = PopupDialog(title: "Success! 🎉",
                                        message: NSLocalizedString("Your account has been confirmed and you're now signed in. You'll get the latest block lists, access to Lockdown Mac, and get critical announcements.", comment: ""),
                                        image: nil,
                                        buttonAlignment: .horizontal,
                                        transitionStyle: .bounceDown,
                                        preferredWidth: 270,
                                        tapGestureDismissal: true,
                                        panGestureDismissal: false,
                                        hideStatusBar: false,
                                        completion: nil)
                popup.addButtons([
                   DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {}
                ])
                self.getCurrentViewController()?.present(popup, animated: true, completion: nil)
                DispatchQueue.main.async {
                    NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self)
                }
            }
            .catch { error in
                var errorMessage = error.localizedDescription
                DDLogError("AppDelegate error: \(errorMessage)")
                if let apiError = error as? ApiError {
                    errorMessage = apiError.message
                }
                
                let popup = PopupDialog(title: "Error Confirming Account",
                                        message: "\(NSLocalizedString("Error while trying to confirm your account:", comment: "")) \(errorMessage). \(NSLocalizedString("If this persists, please contact team@lockdownprivacy.com.", comment: ""))",
                                        image: nil,
                                        buttonAlignment: .horizontal,
                                        transitionStyle: .bounceDown,
                                        preferredWidth: 270,
                                        tapGestureDismissal: true,
                                        panGestureDismissal: false,
                                        hideStatusBar: false,
                                        completion: nil)
                popup.addButtons([
                   DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {}
                ])
                self.getCurrentViewController()?.present(popup, animated: true, completion: nil)
            }
        }
        
        return true
    }
    
    // MARK: - Utilities
    // Returns the most recently presented UIViewController (visible)
    func getCurrentViewController() -> UIViewController? {
        return getCurrentViewController(in: UIApplication.shared.keyWindow?.rootViewController)
    }
    
    private func getCurrentViewController(in root: UIViewController?) -> UIViewController? {
        // If the root view is a navigation controller, we can just return the visible ViewController
        if let navigationController = getNavigationController(in: root) {
            return navigationController.visibleViewController
        }
        if let tabBarVC = root as? UITabBarController {
            if let nvc = getNavigationController(in: tabBarVC.selectedViewController) {
                return nvc.visibleViewController
            } else if let selected = tabBarVC.selectedViewController {
                return selected
            }
        }
        // Otherwise, we must get the root UIViewController and iterate through presented views
        if let rootController = root {
            var currentController: UIViewController! = rootController
            // Each ViewController keeps track of the view it has presented, so we
            // can move from the head to the tail, which will always be the current view
            while( currentController.presentedViewController != nil ) {
                currentController = currentController.presentedViewController
            }
            return currentController
        }
        return nil
    }

    // Returns the navigation controller if it exists
    func getNavigationController(in root: UIViewController?) -> UINavigationController? {
        if let navigationController = root {
            return navigationController as? UINavigationController
        }
        return nil
    }

}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert, .sound])
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let identifier = PushNotifications.Identifier(rawValue: response.notification.request.identifier)
        if identifier.isWeeklyUpdate {
            showUpdateBlockListsFlow()
        } else if identifier == .onboarding {
            highlightBlockLogOnHomeVC()
        }
        completionHandler()
    }
    
    private func highlightBlockLogOnHomeVC() {
        if let hvc = self.getCurrentViewController() as? HomeViewController {
            hvc.highlightBlockLog()
        }
    }
    
    private func showUpdateBlockListsFlow() {
        // the actual update happens in `appHasJustBeenUpgradedOrIsNewInstall`,
        // these are supporting visuals
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
            self.showUpdatingBlockListsLoader()
            DispatchQueue.main.asyncAfter(deadline: .now() + 2.25) {
                self.hideUpdatingBlockListsLoader()
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
                    self.showBlockListsUpdatedPopup()
                }
            }
        }
    }
    
    private func showUpdatingBlockListsLoader() {
        let activity = ActivityData(
            message: NSLocalizedString("Updating Block Lists", comment: ""),
            messageFont: UIFont(name: "Montserrat-Bold", size: 18),
            type: .ballSpinFadeLoader,
            backgroundColor: UIColor(red: 0, green: 0, blue: 0, alpha: 0.7)
        )
        NVActivityIndicatorPresenter.sharedInstance.startAnimating(activity, NVActivityIndicatorView.DEFAULT_FADE_IN_ANIMATION)
    }
    
    private func hideUpdatingBlockListsLoader() {
        NVActivityIndicatorPresenter.sharedInstance.stopAnimating(NVActivityIndicatorView.DEFAULT_FADE_OUT_ANIMATION)
    }
    
    private func showBlockListsUpdatedPopup() {
        let popup = PopupDialog(
            title: NSLocalizedString("Update Success", comment: ""),
            message: "You're now protected against the latest trackers. 🎉"
        )
        popup.addButton(DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap:
        true, action: nil))
        self.getCurrentViewController()?.present(popup, animated: true, completion: nil)
    }
}

extension PacketTunnelProviderLogs {
    static func flush() {
        guard !PacketTunnelProviderLogs.allEntries.isEmpty else {
            return
        }
        
        DDLogInfo("Packet Tunnel Provider Logs: START")
        for logEntry in PacketTunnelProviderLogs.allEntries {
            DDLogError(logEntry)
        }
        DDLogInfo("Packet Tunnel Provider Logs: END")
        PacketTunnelProviderLogs.clear()
    }
}
