import Foundation
import CoreBluetooth

protocol BluetoothDeviceDelegate: AnyObject {

    func bluetoothDevice(didUpdate info: DeviceInfo?)
}

actor BluetoothDevice: NSObject, ObservableObject {

    let peripheral: CBPeripheral!

    private let characteristic: CBCharacteristic!

    @Published
    var lastDeviceInfo: DeviceInfo?

    @Published
    private(set) var lastRSSI: Int = 0

    weak var delegate: BluetoothDeviceDelegate?

    func set(delegate: BluetoothDeviceDelegate?) {
        self.delegate = delegate
    }

    init(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
        self.peripheral = peripheral
        self.characteristic = characteristic
        super.init()
        
        peripheral.delegate = self
    }

    override init() {
        self.peripheral = nil
        self.characteristic = nil
        super.init()
    }

    private var requestContinuation: (id: Int, call: CheckedContinuation<Data?, Never>)?

    func updateInfo() async {
        guard let info = await getInfo() else {
            return
        }
        lastDeviceInfo = info
        delegate?.bluetoothDevice(didUpdate: info)
        #warning("Don't use global variable")
        storage.save(deviceInfo: info)
    }

    func getInfo() async -> DeviceInfo? {
        await get(DeviceInfoRequest())
    }

    func getDeviceData(offset: Int, count: Int) async -> Data? {
        await get(DeviceDataRequest(offset: offset, count: count))
    }

    func deleteDeviceData(byteCount: Int) async -> Bool {
        await get(DeviceDataResetRequest(byteCount: byteCount)) != nil
    }

    private func get<Request>(_ request: Request) async -> Request.Response? where Request: DeviceRequest {
        guard requestContinuation == nil else {
            // Prevent parallel requests
            return nil
        }

        let requestData = Data([request.type.rawValue]) + request.payload
        let responseData: Data? = await withCheckedContinuation { continuation in
            let id = Int.random(in: .min...Int.max)
            requestContinuation = (id, continuation)
            peripheral.writeValue(requestData, for: characteristic, type: .withResponse)
            peripheral.readValue(for: characteristic)
            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
                Task {
                    await self?.checkTimeoutForCurrentRequest(request.type, id: id)
                }
            }
        }

        guard let responseData else {
            return nil
        }
        guard let responseCode = responseData.first else {
            log.error("Request \(request.type) got response of zero bytes")
            return nil
        }
        guard let responseType = BluetoothResponseType(rawValue: responseCode) else {
            log.error("Request \(request.type) got unknown response code \(responseCode)")
            return nil
        }
        switch responseType {
        case .success, .responseTooLarge, .invalidNumberOfBytesToDelete:
            break
        case .invalidCommand:
            log.error("Request \(request.type) failed: Invalid command")
            return nil
        case .unknownCommand:
            log.error("Request \(request.type) failed: Unknown command")
            return nil
        case .responseInProgress:
            log.info("Request \(request.type) failed: Device is busy")
            return nil
        }
        return request.makeResponse(from: responseData.dropFirst(), responseType: responseType)
    }

    private func checkTimeoutForCurrentRequest(_ type: BluetoothRequestType, id: Int) {
        guard let requestContinuation else { return }
        guard requestContinuation.id == id else { return }
        log.info("Timed out for request \(type)")
        requestContinuation.call.resume(returning: nil)
        self.requestContinuation = nil
    }
}

extension BluetoothDevice: CBPeripheralDelegate {

    nonisolated
    func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {

    }

    nonisolated
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        Task {
            await didUpdateValue(for: characteristic, of: peripheral, error: error)
        }
    }

    private func didUpdateValue(for characteristic: CBCharacteristic, of peripheral: CBPeripheral, error: Error?) {
        if let error = error {
            log.error("Failed to read value update: \(error)")
            continueRequest(with: nil)
            return
        }

        guard characteristic.uuid == self.characteristic.uuid else {
            log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)")
            continueRequest(with: nil)
            return
        }
        guard let data = characteristic.value else {
            log.warning("No data")
            continueRequest(with: nil)
            return
        }
        continueRequest(with: data)
    }

    private func continueRequest(with response: Data?) {
        guard let requestContinuation else {
            log.error("No continuation to handle request data (\(response?.count ?? 0) bytes)")
            return
        }
        requestContinuation.call.resume(returning: response)
        self.requestContinuation = nil
    }
}
