//
/* NOTICE: All information contained herein is, and remains the property of Brightinsight Inc. or its customer.
The intellectual and technical concepts contained herein are proprietary to Brightinsight Inc. or its customer and may be covered by U.S. and Foreign Patents, patents in process, and are protected by trade secret or copyright law.
Dissemination of this information or reproduction of this material is strictly forbidden unless prior written permission is obtained from Brightinsight Inc. or its customer. Access to the source code contained herein is hereby forbidden to anyone except current Brightinsight Inc. employees, managers or contractors who have executed
Confidentiality and Non-disclosure agreements explicitly covering such access.
*/
/**
* System Name : BISDKCore
* Component ID : BISDKTimedUploader
* Description :
* Author: Arnold Dominguez
* Copyright © : Brightinsight, Inc.
* -----------------------
* Revision Change
* -----------------------
* Author                        Date             Version            Change-Description
 Arnold Dominguez       08/18/21            1.0                 Creation
 Arnold Dominguez       09/17/21            1.1                 Documentation
*========================================================================================================
*/

import Foundation

/// Uploads eligible records from a `BISDKUploader` at a recurring basis.
class BISDKTimedUploader {
    static var logging: BISDKTimedUploader?
    static var deviceMetadata: BISDKTimedUploader?
    static var deviceReading: BISDKTimedUploader?
    
    private var settings: BISDKUploaderRetryConfig
    private var timer = Timer()
    private var uploader: BISDKUploader
    private var recordType: BISDKUploaderRecordType
    private var config: BISDKConfig
    private var apiBuilder: BISDKApiBuilder
    
    init(
        uploader: BISDKUploader,
        recordType: BISDKUploaderRecordType,
        config: BISDKConfig,
        settings: BISDKUploaderRetryConfig
    ) {
        self.uploader = uploader
        self.recordType = recordType
        self.config = config
        self.settings = settings
        self.apiBuilder = BISDKApiBuilder(config)
        
        self.timer = Timer.scheduledTimer(timeInterval: TimeInterval(settings.delay), target: self, selector: #selector(timerFired), userInfo: nil, repeats: true)
        
        clearAllRetries()
    }
    
    deinit {
        timer.invalidate()
    }
}
extension BISDKTimedUploader {
    @objc func timerFired() {
        let newUploadableRecords = getNewUploadableRecords()
        uploadIfNeeded(records: newUploadableRecords)

        removeRecordsWithTooManyTotalRetries()
    }
    
    func path(for type: BISDKUploaderRecordType) -> String {
        switch recordType {
        case .log:
            return "/orc-device-service/device/logs"
        case .deviceMetadata:
            return "/orc-device-service/device/metadata"
        case .deviceObservation:
            return "/orc-device-service/device/observations"
        }
    }
    
    func uploadIfNeeded(records: [BISDKUploaderRecord]) {
        for record in records {
            guard shouldUpload(record: record) else { continue }
            
            upload(record: record) { [weak self] success, error in
                self?.handleUpload(of: record, success: success, error: error)
            }
        }
    }
    
    func shouldUpload(record: BISDKUploaderRecord) -> Bool {
        let totalUploadRetries = record.totalUploadRetries
        let uploadRetries = record.uploadRetries
        
        let isOKByCurrentSession = uploadRetries < settings.maxRetry
        let isOKByAllSessions = totalUploadRetries < settings.totalMaxRetry
        
        return isOKByCurrentSession && isOKByAllSessions
    }
}

// MARK: - Record Upload to Server

extension BISDKTimedUploader {
    func upload(record: BISDKUploaderRecord, completion: @escaping (Bool, Error?) -> Void) {
        let builder : BISDKApiRequest<BISDKUploaderResponse> = apiBuilder.builder()
        
        let task = builder.withMethod("POST")
            .withBody(["payload": parse(payload: record.payload)])
            .addHeaders([
                .authorization: authorization,
            ])
            .addHeaders(headersFor(recordType))
            .addPath(path(for: recordType))
            .build { (_, response, error) in
                let statusCode: Int = (response as? HTTPURLResponse)?.statusCode ?? -1
                
                completion(statusCode >= 200 && statusCode <= 299, error)
            }
        task.resume()
    }

    func handleUpload(of record: BISDKUploaderRecord, success: Bool, error: Error?) {
        if success {
            self.logUploaded(record: record)
            self.removeUploaded(record: record)
        } else {
            self.increaseRetryCountersFor(record: record)
            
            DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(settings.retryDelay)) {
                self.uploadIfNeeded(records: [record])
            }
        }
    }
}

// MARK: - Database

extension BISDKTimedUploader {
    /**
     Returns records that match the following criteria:
     1. Still haven't attempted to be uploaded on the current session
     2. Total upload attempts on all sessions are less than `settings.totalMaxRetry`
     */
    func getNewUploadableRecords() -> [BISDKUploaderRecord] {
        return uploader.getRecords(with: settings.totalMaxRetry, of: recordType)
    }
    
    func increaseRetryCountersFor(record: BISDKUploaderRecord) {
        record.uploadRetries += 1
        record.totalUploadRetries += 1
        record.lastAttempt = Date()
        
        var now = Date()
        if let nextRetry = record.nextRetry {
            now = nextRetry.addingTimeInterval(TimeInterval(settings.retryDelay))
        } else {
            now = now.addingTimeInterval(TimeInterval(settings.retryDelay))
        }
        record.nextRetry = now
        
        uploader.update(record: record)
    }
    
    func removeUploaded(record: BISDKUploaderRecord) {
        uploader.delete(record: record)
    }
    
    /// Removes records that had too many upload retires between all sessions.
    func removeRecordsWithTooManyTotalRetries() {
        uploader.deleteRecords(with: settings.totalMaxRetry, of: recordType)
    }
    
    func clearAllRetries() {
        let records = uploader.getRecords(of: self.recordType)
        for record in records {
            record.uploadRetries = 0
            record.nextRetry = nil
            uploader.update(record: record)
        }
    }
}

extension BISDKTimedUploader: BISDKComponent {
    var coreModule: BISDKCoreModule? { try? getBISDKModule() }
    var authorization: String { coreModule?.authorization ?? "" }
}

private extension BISDKTimedUploader {
    
    func headersFor(
        _ recordType: BISDKUploaderRecordType
    ) -> [BISDKHTTPHeader: String] {
        switch recordType {
        case .log:
            return deviceInfoHeaders
        case .deviceMetadata:
            return [:]
        case .deviceObservation:
            return [:]
        }
    }
    
    var deviceInfoHeaders: [BISDKHTTPHeader:String] {
        var headers: [BISDKHTTPHeader:String] = [:]
        
        BISDKDeviceEnvironmentInfo.toDict().forEach { key, value in
            headers[.custom(key)] = value
        }
        
        return headers
    }
    
    func parse(payload: String) -> [String:AnyObject] {
        guard let data = payload.data(using: .utf8) else { return [:] }
        let dictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject] ?? [:]
        
        return dictionary ?? [:]
    }
    func logUploaded(record: BISDKUploaderRecord) {
        let extras = [
            BISDKLogEvent.CommonKeys.uploadRetries.rawValue: "\(record.uploadRetries)",
            BISDKLogEvent.CommonKeys.totalUploadRetries.rawValue: "\(record.totalUploadRetries)",
            BISDKLogEvent.CommonKeys.recordType.rawValue: record.recordType.rawValue
        ]
        let logEvent = BISDKLogEvent(event: .BISDKCore_uploadComplete, type: .info, date: Date(), extras: extras)
        
        guard
            record.recordType != .log,
            let payload = BISDKLogPayloadEncoder.encode(event: logEvent)
        else { return }
        
        uploader.save(payload: payload, for: .log) { uuid in
            #if DEBUG
            print("*** Log entity created with ID \(uuid) ***")
            #endif
        }
    }
}
