import UIKit
import CoreData
import CoreBluetooth

class BluetraceManager: SensorDelegate {
    
    private let logger = ConcreteSensorLogger(subsystem: "Herald", category: "BluetraceManager")
    
    var sensorDidUpdateStateCallback: ((SensorState, SensorType?) -> Void)?
    
    // Payload data supplier, sensor and contact log
    var payloadDataSupplier: PayloadDataSupplier?
    var sensor: Sensor?
    
    static let shared = BluetraceManager()
    
    var bleSensorState: SensorState = .unavailable
    var awakeSensorState: SensorState = .unavailable
    
    private var didRecordPayloadData: [String:Date] = [:]
    private var didWriteLegacyPayloadData: [String:Date] = [:]
    
    func turnOnBLE() {
        payloadDataSupplier = EncounterMessageManager.shared
        if sensor == nil {
            sensor = SensorArray(payloadDataSupplier!)
            sensor?.add(delegate: self)
        }
        sensor?.start()
    }
    
    func turnOnAllSensors() {
        payloadDataSupplier = EncounterMessageManager.shared
        if sensor == nil {
            sensor = SensorArray(payloadDataSupplier!)
            sensor?.add(delegate: self)
            // we are going to turn on one at a time
            let previousSensorDidUpdateStateCallback = sensorDidUpdateStateCallback
            sensorDidUpdateStateCallback = { state, type in
                if (state == .on ) {
                    self.sensorDidUpdateStateCallback = previousSensorDidUpdateStateCallback
                    self.turnOnLocationSensor()
                }
            }
        }
        sensor?.start()
    }
    
    func addDelegateToSensors(delegate: SensorDelegate) {
        sensor?.add(delegate: delegate)
    }    
    
    func turnOnLocationSensor() {
        guard let sensorArray = sensor as? SensorArray else {
            return
        }
        DispatchQueue.main.async {
            sensorArray.startAwakeSensor()
        }
    }
    
    func isBluetoothAuthorized() -> Bool {
        if #available(iOS 13.1, *) {
            return CBManager.authorization == .allowedAlways
        } else {
            return CBPeripheralManager.authorizationStatus() == .authorized
        }
    }
    
    func isBluetoothOn() -> Bool {
        return bleSensorState == .on
    }
    
    func isLocationOnAuthorized() -> Bool {
        return awakeSensorState == .on
    }
    
    func centralDidUpdateStateCallback(_ state: CBManagerState) {
        if state == .poweredOn {
            sensorDidUpdateStateCallback?(.on, .BLE)
        } else {
            sensorDidUpdateStateCallback?(.off, .BLE)
        }
    }
    
    func toggleAdvertisement(_ state: Bool) {
        if state {
            sensor?.start()
        } else {
            sensor?.stop()
        }
    }
    
    func toggleScanning(_ state: Bool) {
        if state {
            sensor?.start()
        } else {
            sensor?.stop()
        }
    }
    
    func cleanRecordsData( records: inout [String:Date], expiryInterval: TimeInterval) {
        let removeDataBefore = Date() - expiryInterval
        let recentDidWritePayloadData = records.filter({ $0.value >= removeDataBefore })
        records = recentDidWritePayloadData
    }
    
    func saveEncounter(payload: PayloadData, proximity: Proximity, txPower: Int? ) throws {
        let peripheralCharData = try JSONDecoder().decode(PeripheralCharacteristicsData.self, from: payload)
        var encounterStruct = EncounterRecord(rssi: proximity.value, txPower: txPower == nil ? nil : Double(txPower!))
        encounterStruct.msg = peripheralCharData.msg
        encounterStruct.update(modelP: peripheralCharData.modelP)
        encounterStruct.org = peripheralCharData.org
        encounterStruct.v = peripheralCharData.v
        encounterStruct.timestamp = Date()
        // here the remote blob will be msg and modelp if v1, msg if v2
        // local blob will be rssi, txpower, modelc
        try encounterStruct.saveRemotePeripheralToCoreData()
    }
    
    func getIdentifier(forDevice: BLEDevice) -> String {
        return forDevice.pseudoDeviceAddress != nil ? "\(forDevice.pseudoDeviceAddress!.address)" : forDevice.identifier
    }
    
    func shouldSaveEncounter(forDeviceIdentifier: String) -> Bool {
        
        guard didRecordPayloadData[forDeviceIdentifier] == nil || abs(didRecordPayloadData[forDeviceIdentifier]!.timeIntervalSinceNow) > BluetraceConfig.PeripheralPayloadSaveInterval else {
            return false
        }
        return true
    }
    
    // MARK:- SensorDelegate
    
    func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
        logger.info(sensor.rawValue + ",didDetect=" + didDetect.description)
    }
    
    func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
        logger.info(sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description)
        
        // Make a call to the messages API if needed.
        // Dispatch in background queue
        DispatchQueue.global(qos: .background).async {
            MessageAPI.getMessagesIfNeeded() { (messageResponse, error) in
                if let error = error {
                    DLog("Get messages error: \(error.localizedDescription)")
                }
                // We currently dont do anything with the response. Messages are delivered via APN
            }
        }
    }
    
    func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData, forDevice: BLEDevice) {
        logger.info(sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description + ",withPayload=" + withPayload.shortName)
        
        let deviceIdentifier = getIdentifier(forDevice: forDevice)
        guard shouldSaveEncounter(forDeviceIdentifier: deviceIdentifier) else {
            logger.info("didMeasure, skipping save as time since last saved too recet fromTarget=" + fromTarget.description)
            return
        }
        didRecordPayloadData[deviceIdentifier] = Date()
        cleanRecordsData(records: &didRecordPayloadData, expiryInterval: BluetraceConfig.PeripheralPayloadExpiry)
        do {
            logger.debug("Saving on didMeasure fromTarget=" + fromTarget.description)
            try saveEncounter(payload: withPayload, proximity: didMeasure, txPower: nil)
        } catch {
            logger.fault("ERROR "+sensor.rawValue + ",didMeasure=" + didMeasure.description + ",fromTarget=" + fromTarget.description + ",withPayload=" + withPayload.shortName )
        }

    }
    
    func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
        do {
            let dataFromCentral = try JSONDecoder().decode(CentralWriteData.self, from: didRead)
            logger.info(sensor.rawValue + ",didRead=" + dataFromCentral.msg + ",fromTarget=" + fromTarget.description)
        } catch {
            logger.fault(sensor.rawValue + ",didRead=" + didRead.shortName + ",fromTarget=" + fromTarget.description)
        }
    }
    
    func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
        logger.info(sensor.rawValue + ",didRead=\(didRead.shortName))" + ",fromTarget=" + fromTarget.description + ",atProximity=" + atProximity.description + ",withTxPower=\(String(describing: withTxPower))")
    }
    
    func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
        let payloads = didShare.map { $0.shortName }
        logger.info(sensor.rawValue + ",didShare=" + payloads.description + ",fromTarget=" + fromTarget.description)
        
    }
        
    func sensor(_ sensor: SensorType, didUpdateState: SensorState) {
        logger.info(sensor.rawValue + ",didUpdateState=" + didUpdateState.rawValue)
        
        if sensor == .BLE {
            bleSensorState = didUpdateState
            sensorDidUpdateStateCallback?(didUpdateState, sensor)
        }
        
        if sensor == .AWAKE {
            awakeSensorState = didUpdateState
            sensorDidUpdateStateCallback?(didUpdateState, sensor)
        }
    }
    
    func shouldWriteToLegacyDevice(_ device: BLEDevice) -> Bool {
        
        guard device.legacyPayloadCharacteristic != nil &&
                device.payloadCharacteristic == nil else {
            return false
        }
        
        let cleanInterval = BluetraceConfig.PeripheralPayloadExpiry
        let writeInterval = BluetraceConfig.PeripheralPayloadSaveInterval
        let deviceIdentifier = getIdentifier(forDevice: device)
        cleanRecordsData(records: &didWriteLegacyPayloadData, expiryInterval: cleanInterval)
        
        guard didWriteLegacyPayloadData[deviceIdentifier] == nil || abs(didWriteLegacyPayloadData[deviceIdentifier]!.timeIntervalSinceNow) > writeInterval else {
            return false
        }
        return true
    }
    
    func didWriteToLegacyDevice(_ device: BLEDevice) {
        let deviceIdentifier = getIdentifier(forDevice: device)
        didWriteLegacyPayloadData[deviceIdentifier] = Date()
    }
}