import Foundation
import SwiftUI
import CoreBluetooth

final class BluetoothScanner: NSObject, CBCentralManagerDelegate, ObservableObject {

    enum ConnectionState {
        case noDeviceFound
        case connecting
        case discoveringService
        case discoveringCharacteristic
    }

    private let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")

    private let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002")

    private var manager: CBCentralManager! = nil

    @Published
    var bluetoothIsAvailable = false

    @Published
    var connectionState: ConnectionState

    @Published
    var configuredDevice: BluetoothDevice?

    @Published
    var lastDeviceInfo: DeviceInfo?

    private var connectingDevice: CBPeripheral?

    @Published
    var isScanningForDevices: Bool = false {
        didSet {
            if isScanningForDevices {
                startScanning()
            } else {
                stopScanning()
            }
        }
    }

    private func startScanning() {
        guard !manager.isScanning else {
            return
        }
        manager.scanForPeripherals(withServices: [serviceUUID])
        log.info("Scanner: Started scanning for devices")
    }

    private func stopScanning() {
        guard manager.isScanning else {
            return
        }
        manager.stopScan()
        log.info("Scanner: Stopped scanning for devices")
    }

    var isConnectingOrConnected: Bool {
        configuredDevice != nil || connectingDevice != nil
    }

    func disconnect() {
        if let configuredDevice {
            manager.cancelPeripheralConnection(configuredDevice.peripheral)
        }
        if let connectingDevice {
            manager.cancelPeripheralConnection(connectingDevice)
        }
    }

    override init() {
        connectionState = .noDeviceFound
        super.init()
        self.manager = CBCentralManager(delegate: self, queue: nil)
        self.isScanningForDevices = manager.isScanning
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        guard connectionState == .noDeviceFound && configuredDevice == nil && connectingDevice == nil else {
            log.info("Scanner: Discovered additional device '\(peripheral.name ?? "No Name")'")
            return
        }
        log.info("Scanner: Connecting to discovered device '\(peripheral.name ?? "No Name")'")
        connectingDevice = peripheral
        manager.connect(peripheral)
    }

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOff:
            break
        case .poweredOn:
            bluetoothIsAvailable = true
            return
        case .unsupported:
            log.info("Bluetooth state: Not supported")
        case .unknown:
            log.info("Bluetooth state: Unknown")
        case .resetting:
            log.info("Bluetooth state: Resetting")
        case .unauthorized:
            log.info("Bluetooth state: Not authorized")
        @unknown default:
            log.warning("Bluetooth state: Unknown (\(central.state))")
        }
        bluetoothIsAvailable = false
        // TODO: Disconnect devices?
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        log.info("Scanner: Connected to '\(peripheral.name ?? "No Name")'")

        connectionState = .discoveringService
        peripheral.delegate = self
        peripheral.discoverServices([serviceUUID])
        connectingDevice = peripheral
        configuredDevice = nil
        isScanningForDevices = false
    }

    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        log.info("Scanner: Disconnected from '\(peripheral.name ?? "No Name")'")
        connectionState = .noDeviceFound
        configuredDevice = nil
        connectingDevice = nil
        // TODO: Check if peripheral matches the connected device(s)
    }

    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        log.warning("Scanner: Failed to connect to device '\(peripheral.name ?? "No Name")' (\(error?.localizedDescription ?? "No error"))")
        isScanningForDevices = true
        connectionState = .noDeviceFound
        connectingDevice = nil
    }
}

extension BluetoothScanner: CBPeripheralDelegate {

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services, !services.isEmpty else {
            log.error("Connected device '\(peripheral.name ?? "No Name")': No services found")
            manager.cancelPeripheralConnection(peripheral)
            connectionState = .noDeviceFound
            connectingDevice = nil
            isScanningForDevices = true
            return
        }
        guard let service = services.first(where: { $0.uuid.uuidString == serviceUUID.uuidString }) else {
            log.error("Connected device '\(peripheral.name ?? "No Name")': Required service not found: \(services.map { $0.uuid.uuidString})")
            manager.cancelPeripheralConnection(peripheral)
            connectionState = .noDeviceFound
            connectingDevice = nil
            isScanningForDevices = true
            return
        }
        peripheral.delegate = self
        peripheral.discoverCharacteristics([characteristicUUID], for: service)
        connectionState = .discoveringCharacteristic
        connectingDevice = peripheral
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let error = error {
            log.error("Failed to discover characteristics: \(error)")
            manager.cancelPeripheralConnection(peripheral)
            connectionState = .noDeviceFound
            connectingDevice = nil
            isScanningForDevices = true
            return
        }

        guard let characteristics = service.characteristics, !characteristics.isEmpty else {
            log.error("Connected device '\(peripheral.name ?? "No Name")': No characteristics found")
            manager.cancelPeripheralConnection(peripheral)
            connectionState = .noDeviceFound
            connectingDevice = nil
            isScanningForDevices = true
            return
        }

        var desiredCharacteristic: CBCharacteristic? = nil
        for characteristic in characteristics {
            guard characteristic.uuid == characteristicUUID else {
                log.warning("Connected device '\(peripheral.name ?? "No Name")': Unused characteristic \(characteristic.uuid.uuidString)")
                continue
            }
            desiredCharacteristic = characteristic
        }

        connectionState = .noDeviceFound
        connectingDevice = nil

        guard let desiredCharacteristic else {
            log.error("Connected device '\(peripheral.name ?? "No Name")': Characteristic not found")
            manager.cancelPeripheralConnection(peripheral)
            isScanningForDevices = true
            return
        }

        configuredDevice = .init(peripheral: peripheral, characteristic: desiredCharacteristic)
        Task {
            await configuredDevice?.set(delegate: self)
        }
    }
}

extension BluetoothScanner: BluetoothDeviceDelegate {

    func bluetoothDevice(didUpdate info: DeviceInfo?) {
        DispatchQueue.main.async {
            self.lastDeviceInfo = info
        }
    }
}
