//
//  AppDelegate.swift
//  Tusker
//
//  Created by Shadowfacts on 8/15/18.
//  Copyright © 2018 Shadowfacts. All rights reserved.
//

import UIKit
import CoreData
import OSLog
import Sentry
import UserAccounts
import ComposeUI
import TuskerPreferences

typealias Preferences = TuskerPreferences.Preferences

let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate")

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        configureSentry()
        swizzleStatusBar()
        swizzlePresentationController()
        
        AppShortcutItem.createItems(for: application)
        
        DispatchQueue.global(qos: .userInitiated).async {
            AudioSessionHelper.disable()
            AudioSessionHelper.setDefault()
        }
        
        if let oldSavedData = SavedDataManager.load() {
            do {
                for account in oldSavedData.accountIDs {
                    guard let account = UserAccountsManager.shared.getAccount(id: account) else {
                        continue
                    }
                    let controller = MastodonController.getForAccount(account)
                    try oldSavedData.migrateToCoreData(accountID: account.id, context: controller.persistentContainer.viewContext)
                    if controller.persistentContainer.viewContext.hasChanges {
                        try controller.persistentContainer.viewContext.save()
                    }
                }
                try SavedDataManager.destroy()
            } catch {
                // no-op
            }
        }
        
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let oldPreferencesFile = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
        if FileManager.default.fileExists(atPath: oldPreferencesFile.path) {
            if case .failure(let error) = Preferences.migrate(from: oldPreferencesFile) {
                SentrySDK.capture(error: error)
            }
        }
        
        // make sure the persistent container is initialized on the main thread
        // otherwise initializing it on the background thread can deadlock with accessing it on the main thread elsewhere
        _ = DraftsPersistentContainer.shared
        
        DispatchQueue.global(qos: .userInitiated).async {
            let oldDraftsFile = documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
            let appGroupDraftsFile = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!.appendingPathComponent("drafts").appendingPathExtension("plist")
            for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) {
                DraftsPersistentContainer.shared.migrate(from: url) {
                    if case .failure(let error) = $0 {
                        SentrySDK.capture(error: error)
                    }
                }
            }
        }
        
        BackgroundManager.shared.registerHandlers()
        
        return true
    }
    
    private func configureSentry() {
        guard let dsn = Bundle.main.object(forInfoDictionaryKey: "SentryDSN") as? String,
              !dsn.isEmpty else {
            return
        }
        SentrySDK.start { options in
            #if DEBUG
            options.debug = true
            options.environment = "dev"
            #endif
            
            // the '//' in the full url can't be escaped, so we have to add the scheme back
            options.dsn = "https://\(dsn)"
            
            options.enableSwizzling = false
            // required to support releases/release health
            options.enableAutoSessionTracking = true
            options.enableWatchdogTerminationTracking = false
            options.enableAutoPerformanceTracing = false
            options.enableNetworkTracking = false
            options.enableAppHangTracking = false
            options.enableCoreDataTracing = false
            // we don't care about events like battery, keyboard show/hide
            options.enableAutoBreadcrumbTracking = false
            options.enableUserInteractionTracing = false
            
            options.beforeSend = { event in
                // just no, why would anyone need this information
                event.context?.removeValue(forKey: "culture")
                return Preferences.shared.reportErrorsAutomatically ? event : nil
            }
        }
        
        if let clazz = NSClassFromString("SentryInstallation"),
           let objClazz = clazz as AnyObject as? NSObject,
           let id = objClazz.value(forKey: "id") as? String {
            logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)")
        }
    }
    
    override func buildMenu(with builder: UIMenuBuilder) {
        
        if builder.system == .main {
            MenuController.buildMainMenu(builder: builder)
        }
    }
    
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let name = getSceneNameForActivity(options.userActivities.first)
        return UISceneConfiguration(name: name, sessionRole: connectingSceneSession.role)
    }
    
    private func getSceneNameForActivity(_ activity: NSUserActivity?) -> String {
        guard let activity = activity,
              let type = UserActivityType(rawValue: activity.activityType) else {
            return "main-scene"
        }
        
        switch type {
        case .mainScene:
            return "main-scene"
            
        case .showConversation,
             .showTimeline,
             .checkNotifications,
             .search,
             .bookmarks,
             .myProfile,
             .showProfile:
            if activity.displaysAuxiliaryScene {
                stateRestorationLogger.info("Using auxiliary scene for \(type.rawValue, privacy: .public)")
                return "auxiliary"
            } else {
                return "main-scene"
            }
            
        case .newPost:
            return "compose"
        }
    }
    
    private func swizzleStatusBar() {
        let selector = Selector(("handleTapAction:"))
        var originalIMP: IMP?
        let imp = imp_implementationWithBlock({ (self: UIStatusBarManager, sender: AnyObject) in
            let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIStatusBarManager, Selector, AnyObject) -> Void).self)
            let exception = catchNSException {
                guard let windowScene = self.perform(Selector(("windowScene"))).takeUnretainedValue() as? UIWindowScene,
                      let xPosition = sender.value(forKey: "xPosition") as? CGFloat,
                      let delegate = windowScene.delegate as? TuskerSceneDelegate else {
                    original(self, selector, sender)
                    return
                }
                switch delegate.handleStatusBarTapped(xPosition: xPosition) {
                case .stop:
                    return
                case .continue:
                    original(self, selector, sender)
                }
            }
            if let exception {
                SentrySDK.capture(exception: exception)
            }
        } as @convention(block) (UIStatusBarManager, AnyObject) -> Void)
        originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@")
        if originalIMP == nil {
            Logging.general.error("Unable to swizzle status bar manager")
        }
    }
    
    @available(iOS, obsoleted: 17.0)
    private func swizzlePresentationController() {
        guard #unavailable(iOS 17.0) else {
            return
        }
        var originalIMP: IMP?
        let imp = imp_implementationWithBlock({ (self: UIPresentationController) in
            let new = UITraitCollection(pureBlackDarkMode: self.presentingViewController.traitCollection.pureBlackDarkMode)
            if let existing = self.overrideTraitCollection {
                self.overrideTraitCollection = UITraitCollection(traitsFrom: [existing, new])
            } else {
                self.overrideTraitCollection = new
            }
            let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIPresentationController) -> Void).self)
            original(self)
        } as (@convention(block) (UIPresentationController) -> Void))
        let sel = Selector(["Necessary", "If", "Traits", "update", "_"].reversed().joined())
        originalIMP = class_replaceMethod(UIPresentationController.self, sel, imp, "v@:")
        if originalIMP == nil {
            Logging.general.error("Unable to swizzle presentation controller")
        }
    }

}
