import SharedTypes
import Foundation
import CoreBluetooth

/// Bluetooth Low Energy — matches the Android contract. ops:
///   - "scan"    : scan ~4s → JSON [{id,name,rssi}] of nearby peripherals.
///   - "connect" : input = device id (the CBPeripheral UUID from scan) → discover services → "connected".
///   - "read"    : input = {"device":id,"service":uuid,"characteristic":uuid} → value (UTF-8 or hex).
/// Needs NSBluetoothAlwaysUsageDescription. Cannot be tested on the simulator (no BLE) — needs a
/// real device near a peripheral.
enum BluetoothPlugin {
    static func handle(op: String, input: String) async -> PluginResponse {
        switch op {
        case "scan": return await BleManager.shared.scan()
        case "connect": return await BleManager.shared.connect(input.trimmingCharacters(in: .whitespaces))
        case "read": return await BleManager.shared.read(input)
        default: return PluginResponse(ok: false, output: "unknown op '\(op)'")
        }
    }
}

// Single central manager for the app's lifetime. All CoreBluetooth callbacks run on the main queue;
// `@unchecked Sendable` because the continuations are resumed there and each is nil-guarded.
final class BleManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, @unchecked Sendable {
    static let shared = BleManager()

    private var central: CBCentralManager!
    private var discovered: [String: CBPeripheral] = [:]
    private var rssis: [String: Int] = [:]
    private var connected: [String: CBPeripheral] = [:]
    private var scanCont: CheckedContinuation<PluginResponse, Never>?
    private var connectCont: CheckedContinuation<PluginResponse, Never>?
    private var readCont: CheckedContinuation<PluginResponse, Never>?

    override init() {
        super.init()
        central = CBCentralManager(delegate: self, queue: .main)
    }

    func centralManagerDidUpdateState(_ central: CBCentralManager) {}

    private func poweredOn() -> Bool { central.state == .poweredOn }

    // The first call happens before CoreBluetooth finishes powering on / the user answers the
    // permission prompt — poll briefly so the first tap works instead of needing a second. But
    // only wait on a transient state (.unknown/.resetting); a terminal state (denied/off/unsupported)
    // returns immediately so the user gets an actionable message, not a 6s hang.
    private func ensurePoweredOn() async -> Bool {
        if central.state == .unknown || central.state == .resetting {
            var waited = 0
            while central.state != .poweredOn && waited < 6000 {
                try? await Task.sleep(nanoseconds: 200_000_000)
                waited += 200
            }
        }
        return poweredOn()
    }

    private func stateMessage() -> String {
        switch central.state {
        case .unauthorized: return "denied"
        case .poweredOff: return "bluetooth off"
        case .unsupported: return "bluetooth unsupported"
        default: return "bluetooth unavailable"
        }
    }

    // MARK: scan
    func scan() async -> PluginResponse {
        guard await ensurePoweredOn() else { return PluginResponse(ok: false, output: stateMessage()) }
        discovered.removeAll(); rssis.removeAll()
        return await withCheckedContinuation { cont in
            scanCont = cont
            central.scanForPeripherals(withServices: nil)
            DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in self?.finishScan() }
        }
    }

    private func finishScan() {
        central.stopScan()
        guard let cont = scanCont else { return }
        scanCont = nil
        let arr: [[String: Any]] = discovered.map { id, p in
            ["id": id, "name": p.name ?? "", "rssi": rssis[id] ?? 0]
        }
        let data = (try? JSONSerialization.data(withJSONObject: arr)) ?? Data("[]".utf8)
        cont.resume(returning: PluginResponse(ok: true, output: String(data: data, encoding: .utf8) ?? "[]"))
    }

    func centralManager(_ c: CBCentralManager, didDiscover p: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        let id = p.identifier.uuidString
        discovered[id] = p
        rssis[id] = RSSI.intValue
    }

    // MARK: connect
    func connect(_ id: String) async -> PluginResponse {
        guard await ensurePoweredOn() else { return PluginResponse(ok: false, output: stateMessage()) }
        guard let p = discovered[id] else { return PluginResponse(ok: false, output: "unknown device — scan first") }
        return await withCheckedContinuation { cont in
            connectCont = cont
            p.delegate = self
            central.connect(p)
            DispatchQueue.main.asyncAfter(deadline: .now() + 8) { [weak self] in
                guard let self = self, let c = self.connectCont else { return }
                self.connectCont = nil
                c.resume(returning: PluginResponse(ok: false, output: "connect timeout"))
            }
        }
    }

    func centralManager(_ c: CBCentralManager, didConnect p: CBPeripheral) {
        connected[p.identifier.uuidString] = p
        p.discoverServices(nil)
    }

    func centralManager(_ c: CBCentralManager, didFailToConnect p: CBPeripheral, error: Error?) {
        guard let cont = connectCont else { return }
        connectCont = nil
        cont.resume(returning: PluginResponse(ok: false, output: error?.localizedDescription ?? "connect failed"))
    }

    func peripheral(_ p: CBPeripheral, didDiscoverServices error: Error?) {
        for s in p.services ?? [] { p.discoverCharacteristics(nil, for: s) }
        guard let cont = connectCont else { return }
        connectCont = nil
        cont.resume(returning: PluginResponse(ok: true, output: "connected"))
    }

    func peripheral(_ p: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {}

    // MARK: read
    func read(_ input: String) async -> PluginResponse {
        guard let data = input.data(using: .utf8),
              let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let id = obj["device"] as? String,
              let svc = obj["service"] as? String,
              let chr = obj["characteristic"] as? String,
              let p = connected[id]
        else { return PluginResponse(ok: false, output: "not connected — connect first") }
        guard let service = p.services?.first(where: { $0.uuid.uuidString.caseInsensitiveCompare(svc) == .orderedSame }),
              let characteristic = service.characteristics?.first(where: { $0.uuid.uuidString.caseInsensitiveCompare(chr) == .orderedSame })
        else { return PluginResponse(ok: false, output: "characteristic not found") }
        return await withCheckedContinuation { cont in
            readCont = cont
            p.readValue(for: characteristic)
            DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
                guard let self = self, let c = self.readCont else { return }
                self.readCont = nil
                c.resume(returning: PluginResponse(ok: false, output: "read timeout"))
            }
        }
    }

    func peripheral(_ p: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        guard let cont = readCont else { return }
        readCont = nil
        if let v = characteristic.value {
            let text = String(data: v, encoding: .utf8)
            let out = (text != nil && !(text!.isEmpty)) ? text! : v.map { String(format: "%02x", $0) }.joined()
            cont.resume(returning: PluginResponse(ok: true, output: out))
        } else {
            cont.resume(returning: PluginResponse(ok: false, output: error?.localizedDescription ?? "no value"))
        }
    }
}
