//
//  PeatViewModel.swift
//  PeatTest
//
//  Main view model coordinating Peat BLE mesh operations
//  Uses CoreBluetooth directly to discover real Peat nodes
//  Peer management and document sync handled by Rust PeatMesh
//

import Foundation
import Combine
import CoreBluetooth

/// Flush stdout after print to ensure logs appear immediately
func log(_ message: String) {
    print(message)
    fflush(stdout)
}

// Rust peat-btle UniFFI bindings are in PeatBridge/peat_apple_ffi.swift
// Functions: getDefaultMeshId(), parsePeatDeviceName(), matchesMesh(), generatePeatDeviceName()
// PeatMeshWrapper: Centralized peer management, document sync, event handling

// MARK: - Peat Service UUIDs

/// Peat BLE Service UUID (canonical 128-bit UUID)
/// Must match: f47ac10b-58cc-4372-a567-0e02b2c3d479
let PEAT_SERVICE_UUID = CBUUID(string: "F47AC10B-58CC-4372-A567-0E02B2C3D479")

/// Peat Sync Data Characteristic UUID (canonical)
/// Must match: f47a0003-58cc-4372-a567-0e02b2c3d479
let PEAT_DOC_CHAR_UUID = CBUUID(string: "F47A0003-58CC-4372-A567-0E02B2C3D479")

// MARK: - BLE Manager

/// CoreBluetooth manager for Peat BLE scanning, connections, and advertising
class PeatBLEManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, CBPeripheralManagerDelegate {
    private var centralManager: CBCentralManager!
    private var peripheralManager: CBPeripheralManager!
    private var discoveredPeripherals: [String: CBPeripheral] = [:]
    private var connectedPeripherals: [String: CBPeripheral] = [:]  // Peripherals we connected to as Central
    private var subscribedCentrals: [CBCentral] = []  // Centrals subscribed to our notifications
    private var peatService: CBMutableService?
    private var syncDataCharacteristic: CBMutableCharacteristic?

    /// Local node ID and device name for advertising
    var localNodeId: UInt32 = 0
    var localDeviceName: String = "PEAT-00000000"

    var onStateChanged: ((CBManagerState) -> Void)?
    var onPeerDiscovered: ((String, String?, Int, Data?, Data?) -> Void)?  // identifier, name, rssi, manufacturerData, serviceData
    var onPeerConnected: ((String) -> Void)?
    var onPeerDisconnected: ((String) -> Void)?
    var onDataReceived: ((String, Data) -> Void)?

    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
        peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
    }

    var state: CBManagerState {
        centralManager.state
    }

    // MARK: - Peripheral (Advertising) Mode

    private func setupGattService() {
        syncDataCharacteristic = CBMutableCharacteristic(
            type: PEAT_DOC_CHAR_UUID,
            properties: [.read, .write, .notify],
            value: nil,
            permissions: [.readable, .writeable]
        )
        peatService = CBMutableService(type: PEAT_SERVICE_UUID, primary: true)
        peatService?.characteristics = [syncDataCharacteristic!]
        peripheralManager.add(peatService!)
    }

    private func startAdvertising() {
        guard peripheralManager.state == .poweredOn else { return }
        let advertisementData: [String: Any] = [
            CBAdvertisementDataLocalNameKey: localDeviceName,
            CBAdvertisementDataServiceUUIDsKey: [PEAT_SERVICE_UUID]
        ]
        peripheralManager.startAdvertising(advertisementData)
    }

    func stopAdvertising() {
        peripheralManager.stopAdvertising()
    }

    // MARK: - CBPeripheralManagerDelegate

    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        if peripheral.state == .poweredOn {
            setupGattService()
        }
    }

    func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
        if error == nil {
            startAdvertising()
        }
    }

    func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
    }

    func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
        if request.characteristic.uuid == PEAT_DOC_CHAR_UUID {
            var nodeId = localNodeId
            let data = Data(bytes: &nodeId, count: 4)
            request.value = data
            peripheral.respond(to: request, withResult: .success)
        } else {
            peripheral.respond(to: request, withResult: .attributeNotFound)
        }
    }

    func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
        for request in requests {
            if let data = request.value {
                onDataReceived?("peripheral", data)
            }
            peripheral.respond(to: request, withResult: .success)
        }
    }

    func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
        if !subscribedCentrals.contains(where: { $0.identifier == central.identifier }) {
            subscribedCentrals.append(central)
            log("[BLE] Central subscribed (total: \(subscribedCentrals.count))")
        }
    }

    func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
        subscribedCentrals.removeAll { $0.identifier == central.identifier }
    }

    /// Send data to all connected peers (both as Central and Peripheral)
    func sendData(_ data: Data) {
        var sent = 0

        // Send to subscribed centrals (when we're acting as Peripheral)
        if let characteristic = syncDataCharacteristic, !subscribedCentrals.isEmpty {
            let success = peripheralManager.updateValue(data, for: characteristic, onSubscribedCentrals: nil)
            if success { sent += subscribedCentrals.count }
            log("[BLE] Notify → \(subscribedCentrals.count) centrals (success=\(success))")
        }

        // Send to connected peripherals (when we're acting as Central)
        for (_, peripheral) in connectedPeripherals {
            if let services = peripheral.services,
               let peatService = services.first(where: { $0.uuid == PEAT_SERVICE_UUID }),
               let chars = peatService.characteristics,
               let syncChar = chars.first(where: { $0.uuid == PEAT_DOC_CHAR_UUID }) {
                peripheral.writeValue(data, for: syncChar, type: .withResponse)
                sent += 1
                log("[BLE] Write → \(peripheral.name ?? "?") (\(data.count) bytes)")
            }
        }

        if sent == 0 {
            log("[BLE] WARNING: No peers to send to! (peripherals=\(connectedPeripherals.count), centrals=\(subscribedCentrals.count))")
        }
    }

    // MARK: - Central (Scanning) Mode

    func startScanning() {
        guard centralManager.state == .poweredOn else { return }
        centralManager.scanForPeripherals(
            withServices: [PEAT_SERVICE_UUID],
            options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
        )
    }

    func stopScanning() {
        centralManager.stopScan()
    }

    func connect(identifier: String) {
        guard let peripheral = discoveredPeripherals[identifier] else { return }
        centralManager.connect(peripheral, options: nil)
    }

    func disconnect(identifier: String) {
        guard let peripheral = discoveredPeripherals[identifier] else { return }
        centralManager.cancelPeripheralConnection(peripheral)
    }

    // MARK: - CBCentralManagerDelegate

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        onStateChanged?(central.state)
        if central.state == .poweredOn {
            startScanning()
        }
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
                        advertisementData: [String: Any], rssi RSSI: NSNumber) {
        let identifier = peripheral.identifier.uuidString
        let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String
        let rssi = RSSI.intValue

        // Get manufacturer data (contains node ID on some devices)
        let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data

        // Get service data (Android Peat puts node ID here)
        var serviceData: Data? = nil
        if let serviceDataDict = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
            serviceData = serviceDataDict[PEAT_SERVICE_UUID]
            if serviceData == nil {
                serviceData = serviceDataDict[CBUUID(string: "f47ac10b-58cc-4372-a567-0e02b2c3d479")]
            }
        }

        // Store peripheral reference for connection
        discoveredPeripherals[identifier] = peripheral
        onPeerDiscovered?(identifier, name, rssi, manufacturerData, serviceData)
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        let identifier = peripheral.identifier.uuidString
        peripheral.delegate = self
        connectedPeripherals[identifier] = peripheral
        peripheral.discoverServices([PEAT_SERVICE_UUID])
        onPeerConnected?(identifier)
    }

    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        let identifier = peripheral.identifier.uuidString
        connectedPeripherals.removeValue(forKey: identifier)
        onPeerDisconnected?(identifier)
    }

    var onConnectionFailed: ((String) -> Void)?

    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        onConnectionFailed?(peripheral.identifier.uuidString)
    }

    // MARK: - CBPeripheralDelegate

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }
        for service in services {
            peripheral.discoverCharacteristics([PEAT_DOC_CHAR_UUID], for: service)
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let characteristics = service.characteristics else { return }
        for char in characteristics {
            if char.uuid == PEAT_DOC_CHAR_UUID {
                log("[BLE] Found char props=\(char.properties.rawValue) (write=\(char.properties.contains(.write)), writeNoRsp=\(char.properties.contains(.writeWithoutResponse)))")
                peripheral.setNotifyValue(true, for: char)
                peripheral.readValue(for: char)
            }
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        guard let data = characteristic.value else { return }
        onDataReceived?(peripheral.identifier.uuidString, data)
    }

    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
            log("[BLE] Write FAILED: \(error.localizedDescription)")
        } else {
            log("[BLE] Write confirmed OK")
        }
    }
}

// MARK: - MeshEventHandler

/// Bridge from Rust MeshEventCallback to Swift @MainActor updates
class MeshEventHandler: MeshEventCallback {
    weak var viewModel: PeatViewModel?

    init(viewModel: PeatViewModel) {
        self.viewModel = viewModel
    }

    func onEvent(event: MeshEvent) {
        // Dispatch to main actor for UI updates
        Task { @MainActor [weak self] in
            self?.viewModel?.handleMeshEvent(event)
        }
    }
}

// MARK: - PeatViewModel

/// Main view model for Peat BLE mesh operations
/// CoreBluetooth handling remains in Swift, but peer management
/// and document sync are delegated to Rust PeatMeshWrapper
@MainActor
class PeatViewModel: ObservableObject {
    // MARK: - Constants

    /// UserDefaults key for persisted node ID
    private static let nodeIdKey = "peat_node_id"

    /// Mesh ID - identifies which Peat mesh this node belongs to
    /// Nodes only auto-connect to peers with the same mesh ID
    /// Format: 4-character alphanumeric (e.g., "DEMO", "ALFA", "TEST")
    /// This is provided by the Rust peat-btle crate via UniFFI
    static let MESH_ID: String = getDefaultMeshId()

    /// Get or generate a persistent node ID
    /// Uses last 4 bytes of a generated UUID, similar to MAC-based derivation
    private static func getOrCreateNodeId() -> UInt32 {
        let defaults = UserDefaults.standard

        // Check if we have a saved node ID
        if defaults.object(forKey: nodeIdKey) != nil {
            return UInt32(bitPattern: Int32(truncatingIfNeeded: defaults.integer(forKey: nodeIdKey)))
        }

        // Generate new node ID from UUID (similar to MAC derivation - use last 4 bytes)
        let uuid = UUID()
        let uuidBytes = withUnsafeBytes(of: uuid.uuid) { Array($0) }
        // Use bytes 12-15 (last 4 bytes) like NodeId::from_mac_address uses last 4 of MAC
        let nodeId = (UInt32(uuidBytes[12]) << 24)
                   | (UInt32(uuidBytes[13]) << 16)
                   | (UInt32(uuidBytes[14]) << 8)
                   | UInt32(uuidBytes[15])

        // Persist it
        defaults.set(Int(Int32(bitPattern: nodeId)), forKey: nodeIdKey)
        print("[PeatDemo] Generated new persistent node ID: \(String(format: "%08X", nodeId))")

        return nodeId
    }

    /// Local node ID (persistent across app launches)
    static let NODE_ID: UInt32 = getOrCreateNodeId()

    // MARK: - Published State

    /// Peers in the mesh (derived from PeatMesh)
    @Published var peers: [PeatPeer] = []

    /// Current mesh status message
    @Published var statusMessage: String = "Initializing..."

    /// Whether mesh is active
    @Published var isMeshActive: Bool = false

    /// Alert tracking state
    @Published var ackStatus: AckStatus = AckStatus()

    /// Toast message to display temporarily
    @Published var toastMessage: String?

    /// Bluetooth state
    @Published var bluetoothState: LocalBluetoothState = .unknown

    /// Track last processed emergency to avoid duplicate triggers
    /// Key: (nodeId, timestamp) identifies a unique emergency
    private var lastProcessedEmergency: (nodeId: UInt32, timestamp: UInt64)?

    /// Local node ID
    let localNodeId: UInt32 = NODE_ID

    // MARK: - Computed Properties

    /// Connected peer count (from PeatMesh)
    var connectedCount: Int {
        Int(peatMesh?.connectedCount() ?? 0)
    }

    /// Total peer count (from PeatMesh)
    var totalPeerCount: Int {
        Int(peatMesh?.peerCount() ?? 0)
    }

    /// Display name for local node (from PeatMesh)
    var localDisplayName: String {
        peatMesh?.deviceName() ?? generatePeatDeviceName(meshId: Self.MESH_ID, nodeId: localNodeId)
    }

    // MARK: - Private Properties

    private var bleManager: PeatBLEManager?
    private var peatMesh: PeatMeshWrapper?
    private var meshEventHandler: MeshEventHandler?
    private var maintenanceTimer: Timer?

    // MARK: - Initialization

    init() {
        log("[Peat] Node: \(String(format: "%08X", localNodeId))")
    }

    /// Initialize and start the Peat mesh
    func startMesh() {
        guard !isMeshActive else { return }

        // Initialize Rust PeatMesh for peer management & document sync
        peatMesh = PeatMeshWrapper(
            nodeId: localNodeId,
            callsign: "SWIFT",
            meshId: Self.MESH_ID,
            peripheralType: .soldierSensor
        )

        // Set up event observer
        meshEventHandler = MeshEventHandler(viewModel: self)
        peatMesh?.addObserver(callback: meshEventHandler!)

        // Initialize BLE manager
        bleManager = PeatBLEManager()

        // Configure for advertising (peripheral mode)
        bleManager?.localNodeId = localNodeId
        bleManager?.localDeviceName = peatMesh?.deviceName() ?? localDisplayName

        bleManager?.onStateChanged = { [weak self] state in
            Task { @MainActor [weak self] in
                self?.handleBLEStateChange(state)
            }
        }

        bleManager?.onPeerDiscovered = { [weak self] identifier, name, rssi, mfgData, svcData in
            Task { @MainActor [weak self] in
                self?.handlePeerDiscovered(identifier: identifier, name: name, rssi: rssi, manufacturerData: mfgData, serviceData: svcData)
            }
        }

        bleManager?.onPeerConnected = { [weak self] identifier in
            Task { @MainActor [weak self] in
                self?.handlePeerConnected(identifier: identifier)
            }
        }

        bleManager?.onPeerDisconnected = { [weak self] identifier in
            Task { @MainActor [weak self] in
                self?.handlePeerDisconnected(identifier: identifier)
            }
        }

        bleManager?.onDataReceived = { [weak self] identifier, data in
            Task { @MainActor [weak self] in
                self?.handleDataReceived(identifier: identifier, data: data)
            }
        }

        bleManager?.onConnectionFailed = { [weak self] identifier in
            Task { @MainActor [weak self] in
                self?.handleConnectionFailed(identifier: identifier)
            }
        }

        isMeshActive = true
        statusMessage = "Scanning for Peat nodes..."

        // Periodic maintenance timer (peer cleanup, sync) - 1 second for responsive connection tracking
        maintenanceTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            Task { @MainActor [weak self] in
                self?.performMaintenance()
            }
        }
    }

    /// Shutdown the mesh
    func shutdown() {
        print("[PeatDemo] Shutting down Peat mesh...")

        maintenanceTimer?.invalidate()
        maintenanceTimer = nil
        bleManager?.stopScanning()
        bleManager?.stopAdvertising()
        bleManager = nil
        meshEventHandler = nil
        peatMesh = nil
        isMeshActive = false
        peers.removeAll()
        ackStatus.reset()
        statusMessage = "Mesh stopped"
    }

    /// Connect to a peer
    func connect(to peer: PeatPeer) {
        bleManager?.connect(identifier: peer.identifier)
        showToast("Connecting to \(peer.displayName)...")
    }

    /// Disconnect from a peer
    func disconnect(from peer: PeatPeer) {
        bleManager?.disconnect(identifier: peer.identifier)
    }

    // MARK: - BLE Event Handlers

    private func handleBLEStateChange(_ state: CBManagerState) {
        switch state {
        case .poweredOn:
            bluetoothState = .poweredOn
            statusMessage = "Mesh active - \(localDisplayName)"
        case .poweredOff:
            bluetoothState = .poweredOff
            statusMessage = "Bluetooth is off"
        case .unauthorized:
            bluetoothState = .unauthorized
            statusMessage = "Bluetooth not authorized"
        case .unsupported:
            bluetoothState = .unsupported
            statusMessage = "Bluetooth not supported"
        default:
            bluetoothState = .unknown
        }
    }

    /// Track which peers we've already logged discovery for
    private static var loggedDiscoveries: Set<UInt32> = []

    private func handlePeerDiscovered(identifier: String, name: String?, rssi: Int, manufacturerData: Data?, serviceData: Data?) {
        guard let mesh = peatMesh else { return }

        // Parse mesh ID from name
        var meshId: String? = nil
        if let name = name, let parsed = parsePeatDeviceName(name: name) {
            meshId = parsed.meshId
        }

        let nowMs = UInt64(Date().timeIntervalSince1970 * 1000)

        // Delegate to PeatMesh - it handles peer tracking, filtering, and deduplication
        if let meshPeer = mesh.onBleDiscovered(
            identifier: identifier,
            name: name,
            rssi: Int8(clamping: rssi),
            meshId: meshId,
            nowMs: nowMs
        ) {
            // Only log first discovery of each peer
            if !Self.loggedDiscoveries.contains(meshPeer.nodeId) {
                Self.loggedDiscoveries.insert(meshPeer.nodeId)
                log("[Peat] Discovered: \(String(format: "%08X", meshPeer.nodeId))")
            }

            // Update local peers list from PeatMesh
            syncPeersFromMesh()

            // Auto-connect if it matches our mesh and isn't ourselves
            if meshPeer.nodeId != localNodeId && mesh.matchesMesh(deviceMeshId: meshId) {
                bleManager?.connect(identifier: identifier)
            }
        }
    }

    private func handlePeerConnected(identifier: String) {
        guard let mesh = peatMesh else { return }
        let nowMs = UInt64(Date().timeIntervalSince1970 * 1000)

        if let nodeId = mesh.onBleConnected(identifier: identifier, nowMs: nowMs) {
            log("[Peat] Connected: \(String(format: "%08X", nodeId))")
            syncPeersFromMesh()
        }
    }

    private func handlePeerDisconnected(identifier: String) {
        guard let mesh = peatMesh else { return }

        if let nodeId = mesh.onBleDisconnected(identifier: identifier, reason: .linkLoss) {
            log("[Peat] Disconnected: \(String(format: "%08X", nodeId))")
            syncPeersFromMesh()
        }
    }

    private func handleConnectionFailed(identifier: String) {
        guard let mesh = peatMesh else { return }
        _ = mesh.onBleDisconnected(identifier: identifier, reason: .connectionFailed)
        syncPeersFromMesh()
    }

    private func handleDataReceived(identifier: String, data: Data) {
        guard let mesh = peatMesh else { return }
        let nowMs = UInt64(Date().timeIntervalSince1970 * 1000)

        // Use different method based on whether identifier is mapped
        // "peripheral" is passed when receiving writes from a Central (our peripheral mode)
        // For this case, use onBleData which extracts source from document
        let result: DataReceivedResult?
        if identifier == "peripheral" {
            result = mesh.onBleData(identifier: identifier, data: data, nowMs: nowMs)
        } else {
            result = mesh.onBleDataReceived(identifier: identifier, data: data, nowMs: nowMs)
        }

        if let result = result {
            syncPeersFromMesh()

            // Check document emergency state (CRDT merge already happened)
            if let status = mesh.getEmergencyStatus() {
                let emergencyKey = (nodeId: status.sourceNode, timestamp: status.timestamp)
                let isNew = lastProcessedEmergency == nil ||
                    lastProcessedEmergency!.nodeId != emergencyKey.nodeId ||
                    lastProcessedEmergency!.timestamp != emergencyKey.timestamp

                if isNew && !ackStatus.isActive {
                    log("[DEBUG] Document emergency: source=\(String(format: "%08X", status.sourceNode)) ts=\(status.timestamp) acked=\(status.ackedCount)/\(status.ackedCount + status.pendingCount)")
                    lastProcessedEmergency = emergencyKey
                    handleEmergencyReceivedFromNode(status.sourceNode)
                } else if !isNew {
                    // Same emergency - sync ACK status from document
                    for peer in peers {
                        if mesh.hasPeerAcked(peerId: peer.nodeId) && ackStatus.pendingAcks[peer.nodeId] != true {
                            log("[DEBUG] Document shows \(String(format: "%08X", peer.nodeId)) has ACKed")
                            ackStatus.pendingAcks[peer.nodeId] = true
                        }
                    }
                    checkAllAcked()
                }
            }

            // Also handle peripheral events for backward compatibility
            if result.isEmergency && !ackStatus.isActive {
                log("[DEBUG] Peripheral emergency: node=\(String(format: "%08X", result.sourceNode)) ts=\(result.eventTimestamp)")
                let isNew = lastProcessedEmergency == nil ||
                    lastProcessedEmergency!.nodeId != result.sourceNode ||
                    lastProcessedEmergency!.timestamp != result.eventTimestamp

                if isNew {
                    lastProcessedEmergency = (result.sourceNode, result.eventTimestamp)
                    handleEmergencyReceivedFromNode(result.sourceNode)
                }
            } else if result.isAck && ackStatus.isActive {
                // ACK from peripheral event
                let emergencyTs = lastProcessedEmergency?.timestamp ?? 0
                if result.eventTimestamp > emergencyTs {
                    handleAckReceivedFromNode(result.sourceNode)
                }
            }
        }
    }

    /// Handle emergency received (called from mesh event or data parsing)
    private func handleEmergencyReceivedFromNode(_ nodeId: UInt32) {
        // Don't re-trigger if already in alert mode for the same emergency
        if ackStatus.isActive && ackStatus.emergencySourceNodeId == nodeId {
            return
        }

        log("[PeatDemo] EMERGENCY from \(String(format: "%08X", nodeId))")

        // Initialize ACK tracking from document state
        ackStatus.pendingAcks.removeAll()
        if let mesh = peatMesh {
            for peer in peers {
                ackStatus.pendingAcks[peer.nodeId] = mesh.hasPeerAcked(peerId: peer.nodeId)
            }
            // Log document status
            if let status = mesh.getEmergencyStatus() {
                log("[DEBUG] Received emergency: source=\(String(format: "%08X", status.sourceNode)) \(status.ackedCount)/\(status.ackedCount + status.pendingCount) acked")
            }
        } else {
            for peer in peers {
                ackStatus.pendingAcks[peer.nodeId] = false
            }
        }
        ackStatus.pendingAcks[localNodeId] = false  // We haven't acked yet
        ackStatus.pendingAcks[nodeId] = true  // Source has implicitly acked
        ackStatus.emergencySourceNodeId = nodeId

        showToast("🚨 EMERGENCY from \(String(format: "PEAT-%08X", nodeId))!")
        statusMessage = "⚠️ EMERGENCY - TAP ACK"
        triggerVibration()
    }

    /// Handle ACK received (called from mesh event or data parsing)
    private func handleAckReceivedFromNode(_ nodeId: UInt32) {
        log("[PeatDemo] ACK from \(String(format: "%08X", nodeId))")

        // Update local ACK status (document state is already merged)
        ackStatus.pendingAcks[nodeId] = true

        // Also check document state for other ACKs
        if let mesh = peatMesh {
            for peer in peers {
                if mesh.hasPeerAcked(peerId: peer.nodeId) {
                    ackStatus.pendingAcks[peer.nodeId] = true
                }
            }

            // Log current status
            if let status = mesh.getEmergencyStatus() {
                log("[DEBUG] Emergency status after ACK: \(status.ackedCount)/\(status.ackedCount + status.pendingCount) acked")
            }
        }

        showToast("✓ ACK from \(String(format: "PEAT-%08X", nodeId))")
        checkAllAcked()
    }

    /// Periodic maintenance - delegates to PeatMesh.tick()
    private func performMaintenance() {
        guard let mesh = peatMesh else { return }
        let nowMs = UInt64(Date().timeIntervalSince1970 * 1000)

        // tick() handles peer cleanup
        _ = mesh.tick(nowMs: nowMs)

        // Always send current document as heartbeat (keeps connection alive for peer tracking)
        let document = mesh.buildDocument()
        bleManager?.sendData(Data(document))

        // Refresh peers from mesh
        syncPeersFromMesh()
    }

    /// Sync local peers array from PeatMesh state
    private func syncPeersFromMesh() {
        guard let mesh = peatMesh else { return }

        let meshPeers = mesh.getPeers()
        peers = meshPeers.map { mp in
            PeatPeer(
                identifier: mp.identifier,
                nodeId: mp.nodeId,
                meshId: mp.meshId,
                advertisedName: mp.name,
                isConnected: mp.isConnected,
                rssi: mp.rssi,
                lastSeen: Date(timeIntervalSince1970: Double(mp.lastSeenMs) / 1000.0)
            )
        }
        // Sort by RSSI (strongest first)
        peers.sort { $0.rssi > $1.rssi }
    }

    /// Handle mesh events from Rust PeatMesh observer
    func handleMeshEvent(_ event: MeshEvent) {
        switch event {
        case .peerDiscovered(_):
            syncPeersFromMesh()
        case .peerConnected(_):
            syncPeersFromMesh()
        case .peerDisconnected(_, _):
            syncPeersFromMesh()
        case .peerLost(_):
            syncPeersFromMesh()
        case .emergencyReceived(_):
            // Handled in handleDataReceived with timestamp deduplication
            break
        case .ackReceived(_):
            // Handled in handleDataReceived
            break
        case .documentSynced(_, _):
            break
        case .meshStateChanged(_, _):
            syncPeersFromMesh()
        }
    }

    // MARK: - User Actions (delegate to PeatMesh)

    /// Send an emergency alert to all peers (using document-based tracking)
    func sendEmergency() {
        guard isMeshActive, let mesh = peatMesh else {
            showToast("Mesh not active")
            return
        }

        print("[PeatDemo] >>> SENDING EMERGENCY (document-based)")

        // Build emergency document via PeatMesh (tracks ACKs in document)
        let timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
        let document = mesh.startEmergencyWithKnownPeers(timestamp: timestamp)
        log("[DEBUG] Created emergency document: \(document.count) bytes")
        bleManager?.sendData(Data(document))

        // Track our own emergency for deduplication
        lastProcessedEmergency = (localNodeId, timestamp)
        log("[DEBUG] Sent emergency with ts=\(timestamp)")

        // Initialize local ACK status (syncs with document state)
        ackStatus.pendingAcks.removeAll()
        for peer in peers {
            ackStatus.pendingAcks[peer.nodeId] = mesh.hasPeerAcked(peerId: peer.nodeId)
        }
        ackStatus.pendingAcks[localNodeId] = true  // We sent it, so we're implicitly acked
        ackStatus.emergencySourceNodeId = localNodeId

        showToast("🚨 EMERGENCY SENT!")
        statusMessage = "⚠️ EMERGENCY - TAP ACK"
    }

    /// Send an ACK (using document-based tracking)
    func sendAck() {
        guard isMeshActive, let mesh = peatMesh else {
            showToast("Mesh not active")
            return
        }

        log("[PeatDemo] >>> SENDING ACK (document-based)")
        log("[PeatDemo] Peers: \(peers.count), connected: \(connectedCount)")

        // Build ACK document via PeatMesh (updates document's ACK map)
        let timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
        if let document = mesh.ackEmergency(timestamp: timestamp) {
            log("[PeatDemo] ACK document: \(document.count) bytes")
            bleManager?.sendData(Data(document))

            // Update local ACK status from document
            ackStatus.pendingAcks[localNodeId] = true
            showToast("✓ ACK sent")

            // Check if all peers have ACKed (from document state)
            if mesh.allPeersAcked() {
                log("[DEBUG] All peers ACK'd (from document)")
                ackStatus.reset()
                statusMessage = "✓ All peers acknowledged"
            } else {
                // Keep tracking - other peers still pending
                if let status = mesh.getEmergencyStatus() {
                    log("[DEBUG] Emergency status: \(status.ackedCount)/\(status.ackedCount + status.pendingCount) acked")
                }
            }
        } else {
            log("[PeatDemo] No active emergency to ACK")
            // Clear local state anyway
            ackStatus.reset()
            statusMessage = "Mesh active - \(localDisplayName)"
            showToast("No emergency to ACK")
        }

        // Keep lastProcessedEmergency so we filter out stale gossip
        log("[DEBUG] After ACK: isActive=\(ackStatus.isActive) lastProcessedEmergency=\(lastProcessedEmergency?.timestamp ?? 0)")
    }

    /// Reset the alert state
    func resetAlert() {
        print("[PeatDemo] >>> RESETTING ALERT")

        peatMesh?.clearEmergency()  // Clear document-based emergency
        peatMesh?.clearEvent()      // Clear peripheral event
        ackStatus.reset()
        lastProcessedEmergency = nil
        statusMessage = "Mesh active - \(localDisplayName)"
        showToast("Alert reset")
    }

    // MARK: - Private Helpers

    private func checkAllAcked() {
        // Check both local state and document state
        let localAllAcked = ackStatus.allAcked
        let docAllAcked = peatMesh?.allPeersAcked() ?? true

        if localAllAcked || docAllAcked {
            ackStatus.reset()
            // IMPORTANT: Keep lastProcessedEmergency to filter out stale gossip
            // A new emergency will have a different timestamp
            log("[DEBUG] All ACK'd (local=\(localAllAcked), doc=\(docAllAcked)) - keeping lastProcessedEmergency=\(lastProcessedEmergency?.timestamp ?? 0)")
            statusMessage = "✓ All peers acknowledged"
        }
    }

    private func showToast(_ message: String) {
        toastMessage = message

        Task {
            try? await Task.sleep(nanoseconds: 2_000_000_000)
            if toastMessage == message {
                toastMessage = nil
            }
        }
    }

    private func triggerVibration() {
        #if os(iOS)
        let generator = UINotificationFeedbackGenerator()
        generator.notificationOccurred(.error)
        #endif
    }
}

// MARK: - Bluetooth State (Local)

/// Local Bluetooth state enum (distinct from UniFFI BluetoothState)
enum LocalBluetoothState: String {
    case unknown = "Unknown"
    case resetting = "Resetting"
    case unsupported = "Unsupported"
    case unauthorized = "Unauthorized"
    case poweredOff = "Powered Off"
    case poweredOn = "Powered On"

    var isReady: Bool {
        self == .poweredOn
    }
}

#if os(iOS)
import UIKit
#endif
