import Foundation
import CocoaLumberjackSwift

enum EventLoggingError {
    case generic
    case network
}

@objc(WMFEventLoggingService)
public class EventLoggingService : NSObject, URLSessionDelegate {
    private struct Key {
        static let isEnabled = "SendUsageReports"
        static let appInstallID = "WMFAppInstallID"
        static let lastLoggedSnapshot = "WMFLastLoggedSnapshot"
        static let appInstallDate = "AppInstallDate"
        static let loggedDaysInstalled = "DailyLoggingStatsDaysInstalled"
        static let lastSuccessfulPost = "LastSuccessfulPost"
    }
    
    private var pruningAge: TimeInterval = 60*60*24*30 // 30 days
    private var sendOnWWANThreshold: TimeInterval = 24 * 60 * 60
    private var postBatchSize = 32
    private var postTimeout: TimeInterval = 60*2 // 2 minutes
    private var postInterval: TimeInterval = 60*10 // 10 minutes
    
    private var debugDisableImmediateSend = false
    
    private let session: Session
    
    private let persistentStoreCoordinator: NSPersistentStoreCoordinator
    private let managedObjectContext: NSManagedObjectContext
    private let operationQueue: OperationQueue
    
    @objc(sharedInstance) public static let shared: EventLoggingService? = {
        let fileManager = FileManager.default
        var permanentStorageDirectory = fileManager.wmf_containerURL().appendingPathComponent("Event Logging", isDirectory: true)
        var didGetDirectoryExistsError = false
        do {
            try fileManager.createDirectory(at: permanentStorageDirectory, withIntermediateDirectories: true, attributes: nil)
        } catch let error {
            DDLogError("EventLoggingService: Error creating permanent cache: \(error)")
        }
        do {
            var values = URLResourceValues()
            values.isExcludedFromBackup = true
            try permanentStorageDirectory.setResourceValues(values)
        } catch let error {
            DDLogError("EventLoggingService: Error excluding from backup: \(error)")
        }
        
        let permanentStorageURL = permanentStorageDirectory.appendingPathComponent("Events.sqlite")
        DDLogDebug("EventLoggingService: Events persistent store: \(permanentStorageURL)")
        
        // SINGLETONTODO
        let eventLoggingService = EventLoggingService(session: MWKDataStore.shared().session, permanentStorageURL: permanentStorageURL)
        if let eventLoggingService = eventLoggingService {
            MWKDataStore.shared().setupAbTestsController(withPersistenceService: eventLoggingService)
        }
        
        return eventLoggingService
    }()
    
    @objc
    public func log(event: [String: Any], schema: String, revision: Int, wiki: String) {
        let event: NSDictionary = ["event": event, "schema": schema, "revision": revision, "wiki": wiki]
        logEvent(event)
    }
    
    public init?(session: Session, permanentStorageURL: URL?) {
        let bundle = Bundle.wmf
        let modelURL = bundle.url(forResource: "EventLogging", withExtension: "momd")!
        let model = NSManagedObjectModel(contentsOf: modelURL)!
        let psc = NSPersistentStoreCoordinator(managedObjectModel: model)
        let options = [NSMigratePersistentStoresAutomaticallyOption: NSNumber(booleanLiteral: true), NSInferMappingModelAutomaticallyOption: NSNumber(booleanLiteral: true)]
        operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        self.session = session
        if let storeURL = permanentStorageURL {
            do {
                try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
            } catch {
                do {
                    try FileManager.default.removeItem(at: storeURL)
                } catch {
                    
                }
                do {
                    try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
                } catch {
                    return nil
                }
            }
        } else {
            do {
                try psc.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: options)
            } catch {
                return nil
            }
        }
        
        self.persistentStoreCoordinator = psc
        self.managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        self.managedObjectContext.persistentStoreCoordinator = psc
        super.init()
    }

    
    @objc
    public func reset() {
        self.resetSession()
        self.resetInstall()
    }

    @objc
    func migrateShareUsageAndInstallIDToUserDefaults() {
        let enabledNumber = libraryValue(for: Key.isEnabled) as? NSNumber
        if enabledNumber != nil {
            UserDefaults.standard.wmf_sendUsageReports = enabledNumber!.boolValue
        } else {
            UserDefaults.standard.wmf_sendUsageReports = false
        }

        UserDefaults.standard.wmf_appInstallId = libraryValue(for: Key.appInstallID) as? String
    }

    @objc
    private func logEvent(_ event: NSDictionary) {
        let now = NSDate()
        perform { moc in
            let record = NSEntityDescription.insertNewObject(forEntityName: "WMFEventRecord", into: self.managedObjectContext) as! EventRecord
            record.event = event
            record.recorded = now
            record.userAgent = WikipediaAppUtils.versionedUserAgent()
            
            DDLogDebug("EventLoggingService: \(record.objectID) recorded!")
            
            self.save(moc)
        }
    }
    
    @objc
    private func tryPostEvents(_ completion: (() -> Void)? = nil) {
        let operation = AsyncBlockOperation { (operation) in
            self.perform { moc in
                let pruneFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "WMFEventRecord")
                pruneFetch.returnsObjectsAsFaults = false
                
                let pruneDate = Date().addingTimeInterval(-(self.pruningAge)) as NSDate
                pruneFetch.predicate = NSPredicate(format: "(recorded < %@) OR (posted != nil) OR (failed == TRUE)", pruneDate)
                let delete = NSBatchDeleteRequest(fetchRequest: pruneFetch)
                delete.resultType = .resultTypeCount
                
                do {
                    let result = try self.managedObjectContext.execute(delete)
                    guard let deleteResult = result as? NSBatchDeleteResult else {
                        DDLogError("EventLoggingService: Could not read NSBatchDeleteResult")
                        return
                    }
                    
                    guard let count = deleteResult.result as? Int else {
                        DDLogError("EventLoggingService: Could not read NSBatchDeleteResult count")
                        return
                    }
                    if count > 0 {
                        DDLogInfo("EventLoggingService: Pruned \(count) events")
                    }
                    
                } catch let error {
                    DDLogError("EventLoggingService: Error pruning events: \(error.localizedDescription)")
                }
                
                let fetch: NSFetchRequest<EventRecord> = EventRecord.fetchRequest()
                fetch.sortDescriptors = [NSSortDescriptor(keyPath: \EventRecord.recorded, ascending: true)]
                fetch.predicate = NSPredicate(format: "(posted == nil) AND (failed != TRUE)")
                fetch.fetchLimit = self.postBatchSize

                var eventRecords: [EventRecord] = []

                do {
                    eventRecords = try moc.fetch(fetch)
                } catch let error {
                    DDLogError(error.localizedDescription)
                }
                
                var wifiOnly = true
                if let lastSuccessNumber = moc.wmf_keyValue(forKey: Key.lastSuccessfulPost)?.value as? NSNumber {
                    let now = CFAbsoluteTimeGetCurrent()
                    let interval = now - CFAbsoluteTime(lastSuccessNumber.doubleValue)
                    if interval > self.sendOnWWANThreshold {
                        wifiOnly = false
                    }
                }

                if !eventRecords.isEmpty {
                    self.postEvents(eventRecords, onlyWiFi: wifiOnly, completion: {
                        operation.finish()
                    })
                } else {
                    operation.finish()
                }
            }
        }
        operationQueue.addOperation(operation)
        guard let completion = completion else {
            return
        }
        let completionBlockOp = BlockOperation(block: completion)
        completionBlockOp.addDependency(operation)
        operationQueue.addOperation(completion)
    }
    
    private func perform(_ block: @escaping (_ moc: NSManagedObjectContext) -> Void) {
        let moc = self.managedObjectContext
        moc.perform {
            block(moc)
        }
    }
    
    private func performAndWait(_ block: (_ moc: NSManagedObjectContext) -> Void) {
        let moc = self.managedObjectContext
        moc.performAndWait {
            block(moc)
        }
    }
    
    private func asyncSave() {
        perform { (moc) in
            self.save(moc)
        }
    }
    
    private func postEvents(_ eventRecords: [EventRecord], onlyWiFi: Bool, completion: @escaping () -> Void) {
        DDLogDebug("EventLoggingService: Posting \(eventRecords.count) events!")
        
        let taskGroup = WMFTaskGroup()
        
        var completedRecordIDs = Set<NSManagedObjectID>()
        var failedRecordIDs = Set<NSManagedObjectID>()
        
        for record in eventRecords {
            let moid = record.objectID
            guard let payload = record.event else {
                failedRecordIDs.insert(moid)
                continue
            }
            taskGroup.enter()
            let userAgent = record.userAgent ?? WikipediaAppUtils.versionedUserAgent()
            submit(payload: payload, userAgent: userAgent, onlyWiFi: onlyWiFi) { (error) in
                if let error = error {
                    if error != .network {
                        failedRecordIDs.insert(moid)
                    }
                } else {
                    completedRecordIDs.insert(moid)
                }
                taskGroup.leave()
            }
        }
        
        
        taskGroup.waitInBackground {
            self.perform { moc in
                let postDate = NSDate()
                for moid in completedRecordIDs {
                    let mo = try? self.managedObjectContext.existingObject(with: moid)
                    guard let record = mo as? EventRecord else {
                        continue
                    }
                    record.posted = postDate
                }
                
                for moid in failedRecordIDs {
                    let mo = try? self.managedObjectContext.existingObject(with: moid)
                    guard let record = mo as? EventRecord else {
                        continue
                    }
                    record.failed = true
                }
                if completedRecordIDs.count == eventRecords.count {
                    self.managedObjectContext.wmf_setValue(NSNumber(value: CFAbsoluteTimeGetCurrent()), forKey: Key.lastSuccessfulPost)
                    DDLogDebug("EventLoggingService: All records succeeded")
                } else {
                    DDLogDebug("EventLoggingService: Some records failed")
                }
                self.save(moc)
                completion()
            }
        }
    }
    
    private func submit(payload: NSObject, userAgent: String, onlyWiFi: Bool, completion: @escaping (EventLoggingError?) -> Void) {
        guard let url = Configuration.current.eventLoggingAPIURL(with: payload) else {
            DDLogError("EventLoggingService: Could not create URL")
            completion(EventLoggingError.generic)
            return
        }
        
        var request = URLRequest(url: url)
        request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
        let session = onlyWiFi ? self.session.wifiOnlyURLSession : self.session.defaultURLSession
        let task = session.dataTask(with: request, completionHandler: { (_, response, error) in
            guard error == nil,
                let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode / 100 == 2 else {
                    if let error = error as NSError?, error.domain == NSURLErrorDomain {
                        completion(EventLoggingError.network)
                    } else {
                        completion(EventLoggingError.generic)
                    }
                    return
            }
            completion(nil)
            // DDLogDebug("EventLoggingService: event \(eventRecord.objectID) posted!")
        })
        task.resume()
    }
    
    // mark stored values
    
    private func save(_ moc: NSManagedObjectContext) {
        guard moc.hasChanges else {
            return
        }
        do {
            try moc.save()
        } catch let error {
            DDLogError("Error saving EventLoggingService managedObjectContext: \(error)")
        }
    }
    
    private var semaphore = DispatchSemaphore(value: 1)
    
    private var libraryValueCache: [String: NSCoding] = [:]
    public func libraryValue(for key: String) -> NSCoding? {
        semaphore.wait()
        defer {
            semaphore.signal()
        }
        var value = libraryValueCache[key]
        if value != nil {
            return value
        }
        
        performAndWait { moc in
            value = managedObjectContext.wmf_keyValue(forKey: key)?.value
            if value != nil {
                libraryValueCache[key] = value
                return
            }
            
            if let legacyValue = UserDefaults.standard.object(forKey: key) as? NSCoding {
                value = legacyValue
                libraryValueCache[key] = legacyValue
                managedObjectContext.wmf_setValue(legacyValue, forKey: key)
                UserDefaults.standard.removeObject(forKey: key)
                save(moc)
            }
        }
    
        return value
    }
    
    public func setLibraryValue(_ value: NSCoding?, for key: String) {
        semaphore.wait()
        defer {
            semaphore.signal()
        }
        libraryValueCache[key] = value
        perform { moc in
            self.managedObjectContext.wmf_setValue(value, forKey: key)
            self.save(moc)
        }
    }
    
    @objc public var isEnabled: Bool {
        return UserDefaults.standard.wmf_sendUsageReports
    }
    
    @objc public var lastLoggedSnapshot: NSCoding? {
        get {
            return libraryValue(for: Key.lastLoggedSnapshot)
        }
        set {
            setLibraryValue(newValue, for: Key.lastLoggedSnapshot)
        }
    }
    
    @objc public var appInstallDate: Date? {
        get {
            var value = libraryValue(for: Key.appInstallDate) as? Date
            if value == nil {
                value = Date()
                setLibraryValue(value as NSDate?, for: Key.appInstallDate)
            }
            return value
        }
        set {
            setLibraryValue(newValue as NSDate?, for: Key.appInstallDate)
        }
    }
    
    @objc public var loggedDaysInstalled: NSNumber? {
        get {
            return libraryValue(for: Key.loggedDaysInstalled) as? NSNumber
        }
        set {
            setLibraryValue(newValue, for: Key.loggedDaysInstalled)
        }
    }
    
    private var _sessionID: String?
    @objc public var sessionID: String? {
        semaphore.wait()
        defer {
            semaphore.signal()
        }
        if _sessionID == nil {
            _sessionID = UUID().uuidString
        }
        return _sessionID
    }
    
    private var _sessionStartDate: Date?
    @objc public var sessionStartDate: Date? {
        semaphore.wait()
        defer {
            semaphore.signal()
        }
        if _sessionStartDate == nil {
            _sessionStartDate = Date()
        }
        return _sessionStartDate
    }
    
    @objc public func resetSession() {
        semaphore.wait()
        defer {
            semaphore.signal()
        }
        _sessionID = nil
        _sessionStartDate = Date()
    }
    
    private func resetInstall() {
        UserDefaults.standard.wmf_appInstallId = nil
        lastLoggedSnapshot = nil
        loggedDaysInstalled = nil
        appInstallDate = nil
    }
}

extension EventLoggingService: PeriodicWorker {
    public func doPeriodicWork(_ completion: @escaping () -> Void) {
        tryPostEvents(completion)
    }
}

extension EventLoggingService: BackgroundFetcher {
    public func performBackgroundFetch(_ completion: @escaping (UIBackgroundFetchResult) -> Void) {
        doPeriodicWork {
            completion(.noData)
        }
    }
}

extension EventLoggingService: ABTestsPersisting {
    
}
