# MultipeerConnectivity to Network.framework Transition Guide

This document explains how to transition an iOS app from MultipeerConnectivity (MPC) to Network.framework for peer-to-peer networking.

---

## Why Transition?

- **Network.framework** provides lower-level control over connections
- Better performance with direct TCP/UDP streams
- More flexible discovery options (can combine with BLE for faster discovery)
- AWDL (Apple Wireless Direct Link) support via `includePeerToPeer = true`
- Future-proof: Network.framework is Apple's modern networking API

---

## Architecture Overview

### MultipeerConnectivity Architecture
```
MCNearbyServiceAdvertiser  →  Advertises service
MCNearbyServiceBrowser     →  Discovers services
MCSession                  →  Manages connections & data transfer
MCPeerID                   →  Identifies peers
```

### Network.framework Architecture
```
NWListener                 →  Advertises service (replaces MCNearbyServiceAdvertiser)
NWBrowser                  →  Discovers services (replaces MCNearbyServiceBrowser)
NWConnection               →  Individual connection per peer (replaces MCSession)
UUID / Custom struct       →  Identifies peers (replaces MCPeerID)
```

---

## New Files to Create

### 1. Models

**`DiscoveredHost.swift`** - Represents a discovered host
```swift
import Foundation

struct DiscoveredHost: Identifiable, Hashable {
    let id: UUID
    let deviceName: String
    let eventName: String
    let discoveredAt: Date
    let endpointID: String  // Unique identifier from NWBrowser

    init(deviceName: String, eventName: String, endpointID: String) {
        self.id = UUID()
        self.deviceName = deviceName
        self.eventName = eventName
        self.discoveredAt = Date()
        self.endpointID = endpointID
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(endpointID)
    }

    static func == (lhs: DiscoveredHost, rhs: DiscoveredHost) -> Bool {
        lhs.endpointID == rhs.endpointID
    }
}
```

**`ConnectedPeer.swift`** - Represents a connected peer
```swift
import Foundation

struct ConnectedPeer: Identifiable, Hashable {
    let id: UUID
    let displayName: String
    let connectedAt: Date
    var isStable: Bool  // For traffic gate pattern

    init(displayName: String, isStable: Bool = false) {
        self.id = UUID()
        self.displayName = displayName
        self.connectedAt = Date()
        self.isStable = isStable
    }
}

enum P2PConnectionState {
    case idle
    case hosting
    case browsing
    case connecting
    case connected
    case disconnected
    case failed(Error)
}
```

### 2. NetworkFraming.swift - TCP Message Framing

Network.framework uses raw TCP streams, so you need length-prefixed framing:

```swift
import Foundation
import Network

enum NetworkFraming {
    static let headerSize = 4  // UInt32 for length
    static let maxMessageSize: UInt32 = 10_485_760  // 10 MB

    /// Frame data with 4-byte length prefix
    static func frameData(_ data: Data) -> Data {
        var length = UInt32(data.count).bigEndian
        var framedData = Data(bytes: &length, count: headerSize)
        framedData.append(data)
        return framedData
    }

    /// Send framed data over connection
    static func send(_ data: Data, over connection: NWConnection, completion: @escaping (Error?) -> Void) {
        let framedData = frameData(data)
        connection.send(content: framedData, completion: .contentProcessed { error in
            completion(error)
        })
    }

    /// Receive framed data from connection
    static func receive(from connection: NWConnection, completion: @escaping (Result<Data, Error>) -> Void) {
        // Read 4-byte header
        connection.receive(minimumIncompleteLength: headerSize, maximumLength: headerSize) { headerData, _, isComplete, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let headerData = headerData, headerData.count == headerSize else {
                completion(.failure(FramingError.incompleteHeader))
                return
            }

            let length = headerData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }

            guard length > 0, length <= maxMessageSize else {
                completion(.failure(FramingError.invalidMessageSize(length)))
                return
            }

            // Read payload
            connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { payloadData, _, _, error in
                if let error = error {
                    completion(.failure(error))
                    return
                }

                guard let payloadData = payloadData else {
                    completion(.failure(FramingError.incompletePayload))
                    return
                }

                completion(.success(payloadData))
            }
        }
    }

    /// Continuous receive loop
    static func startReceiving(from connection: NWConnection, onData: @escaping (Data) -> Void, onError: @escaping (Error) -> Void) {
        receive(from: connection) { result in
            switch result {
            case .success(let data):
                onData(data)
                startReceiving(from: connection, onData: onData, onError: onError)
            case .failure(let error):
                onError(error)
            }
        }
    }
}

enum FramingError: LocalizedError {
    case incompleteHeader
    case incompletePayload
    case invalidMessageSize(UInt32)

    var errorDescription: String? {
        switch self {
        case .incompleteHeader: return "Incomplete header"
        case .incompletePayload: return "Incomplete payload"
        case .invalidMessageSize(let size): return "Invalid size: \(size)"
        }
    }
}
```

### 3. BLEDiscoveryManager.swift (Optional - For Fast Discovery)

BLE advertisement wakes AWDL faster than Bonjour alone:

```swift
import Foundation
import CoreBluetooth
import Combine

@MainActor
class BLEDiscoveryManager: NSObject, ObservableObject {
    @Published var isAdvertising = false
    @Published var isScanning = false

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

    // Use a valid UUID (hex characters only: 0-9, A-F)
    private let serviceUUID = CBUUID(string: "YOUR-UUID-HERE")
    private var peripheralManager: CBPeripheralManager?
    private var centralManager: CBCentralManager?
    private var eventName = ""
    private var discoveredPeripherals: Set<UUID> = []

    // MARK: - Host Mode (Peripheral)

    func startAdvertising(eventName: String) {
        self.eventName = eventName
        peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
    }

    func stopAdvertising() {
        peripheralManager?.stopAdvertising()
        peripheralManager = nil
        isAdvertising = false
    }

    // MARK: - Guest Mode (Central)

    func startScanning() {
        discoveredPeripherals.removeAll()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }

    func stopScanning() {
        centralManager?.stopScan()
        centralManager = nil
        isScanning = false
    }
}

extension BLEDiscoveryManager: CBPeripheralManagerDelegate {
    nonisolated func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        Task { @MainActor in
            if peripheral.state == .poweredOn {
                let truncatedName = String(eventName.prefix(8))  // BLE has 28-byte limit
                peripheral.startAdvertising([
                    CBAdvertisementDataServiceUUIDsKey: [serviceUUID],
                    CBAdvertisementDataLocalNameKey: truncatedName
                ])
                isAdvertising = true
            }
        }
    }
}

extension BLEDiscoveryManager: CBCentralManagerDelegate {
    nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
        Task { @MainActor in
            if central.state == .poweredOn {
                central.scanForPeripherals(withServices: [serviceUUID])
                isScanning = true
            }
        }
    }

    nonisolated func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber) {
        Task { @MainActor in
            guard !discoveredPeripherals.contains(peripheral.identifier) else { return }
            discoveredPeripherals.insert(peripheral.identifier)

            let hostName = advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "Unknown"
            onHostDiscovered?(hostName)
        }
    }
}
```

---

## Key Implementation Patterns

### 1. Network Parameters Configuration

```swift
private func createNetworkParameters() -> NWParameters {
    let parameters = NWParameters.tcp
    parameters.includePeerToPeer = true  // Enables AWDL
    parameters.serviceClass = .responsiveData  // Low-latency

    let options = NWProtocolTCP.Options()
    options.enableKeepalive = true
    options.keepaliveInterval = 30
    parameters.defaultProtocolStack.transportProtocol = options

    return parameters
}
```

### 2. Host Mode - NWListener (replaces MCNearbyServiceAdvertiser)

```swift
func startHosting(eventName: String) {
    let parameters = createNetworkParameters()
    listener = try NWListener(using: parameters)

    // Include device name in TXT record
    let deviceName = UIDevice.current.name
    let txtEntry = "deviceName=\(deviceName)"
    var txtData = Data()
    txtData.append(UInt8(txtEntry.utf8.count))
    txtData.append(contentsOf: txtEntry.utf8)

    listener?.service = NWListener.Service(
        name: eventName,
        type: "_yourservice._tcp",
        txtRecord: txtData
    )

    listener?.stateUpdateHandler = { [weak self] state in
        guard let self else { return }
        Task { @MainActor in
            self.handleListenerState(state)
        }
    }

    listener?.newConnectionHandler = { [weak self] connection in
        guard let self else { return }
        Task { @MainActor in
            self.handleNewConnection(connection)
        }
    }

    listener?.start(queue: .main)
}
```

### 3. Guest Mode - NWBrowser (replaces MCNearbyServiceBrowser)

```swift
func startBrowsing() {
    let parameters = createNetworkParameters()
    browser = NWBrowser(for: .bonjour(type: "_yourservice._tcp", domain: nil), using: parameters)

    browser?.browseResultsChangedHandler = { [weak self] results, changes in
        guard let self else { return }
        Task { @MainActor in
            for change in changes {
                switch change {
                case .added(let result):
                    self.handleHostFound(result)
                case .removed(let result):
                    self.handleHostLost(result)
                default:
                    break
                }
            }
        }
    }

    browser?.start(queue: .main)
}

private func handleHostFound(_ result: NWBrowser.Result) {
    guard case .service(let name, let type, let domain, _) = result.endpoint else { return }

    let endpointID = "\(name).\(type).\(domain)"

    // Extract device name from TXT record
    var deviceName = "Unknown Device"
    if case .bonjour(let txtRecord) = result.metadata {
        if let name = txtRecord["deviceName"] {
            deviceName = name
        }
    }

    let host = DiscoveredHost(deviceName: deviceName, eventName: name, endpointID: endpointID)
    availableHosts[endpointID] = host
}
```

### 4. Connecting to Host (replaces invitePeer)

```swift
func joinHost(_ host: DiscoveredHost) {
    let endpoint = NWEndpoint.service(
        name: host.eventName,
        type: "_yourservice._tcp",
        domain: "local",
        interface: nil
    )

    let parameters = createNetworkParameters()
    hostConnection = NWConnection(to: endpoint, using: parameters)

    hostConnection?.stateUpdateHandler = { [weak self] state in
        guard let self else { return }
        Task { @MainActor in
            self.handleConnectionState(state)
        }
    }

    hostConnection?.start(queue: .main)
}
```

### 5. Handling New Connections (Host Side)

```swift
private func handleNewConnection(_ connection: NWConnection) {
    let peerID = UUID()

    guestConnections[peerID] = connection
    connectedPeers[peerID] = ConnectedPeer(displayName: "Guest", isStable: false)

    connection.stateUpdateHandler = { [weak self] state in
        guard let self else { return }
        Task { @MainActor in
            // Handle state changes
        }
    }

    connection.start(queue: .main)

    // Start receiving data
    NetworkFraming.startReceiving(from: connection) { [weak self] data in
        // Handle received data
    } onError: { [weak self] error in
        // Handle error, remove connection
    }

    // Traffic Gate: Wait before marking as stable
    Task { @MainActor in
        try? await Task.sleep(nanoseconds: 4_000_000_000)  // 4 seconds
        guard connectedPeers[peerID] != nil else { return }
        stablePeers.insert(peerID)
        connectedPeers[peerID]?.isStable = true
        onPeerJoined?(peerID)
    }
}
```

### 6. Broadcasting Data (replaces MCSession.send)

```swift
func broadcastData(_ data: Data) {
    // Traffic Gate: Only send to stable peers
    let stableConnections = guestConnections.filter { stablePeers.contains($0.key) }

    for (_, connection) in stableConnections {
        NetworkFraming.send(data, over: connection) { error in
            if let error = error {
                print("Send failed: \(error)")
            }
        }
    }
}
```

---

## Callback Mapping

| MultipeerConnectivity | Network.framework |
|----------------------|-------------------|
| `session(_:peer:didChange:)` | `connection.stateUpdateHandler` |
| `session(_:didReceive:fromPeer:)` | `NetworkFraming.startReceiving()` |
| `advertiser(_:didReceiveInvitationFromPeer:)` | `listener.newConnectionHandler` |
| `browser(_:foundPeer:withDiscoveryInfo:)` | `browser.browseResultsChangedHandler` (.added) |
| `browser(_:lostPeer:)` | `browser.browseResultsChangedHandler` (.removed) |

---

## Info.plist Updates

Add these keys for BLE discovery (if using):

```xml
<key>UIBackgroundModes</key>
<array>
    <string>bluetooth-central</string>
    <string>bluetooth-peripheral</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Uses Bluetooth to quickly discover nearby sessions.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Uses Bluetooth to advertise sessions to nearby devices.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Uses local network for peer-to-peer connections.</string>
```

Keep existing Bonjour services:
```xml
<key>NSBonjourServices</key>
<array>
    <string>_yourservice._tcp</string>
</array>
```

---

## Important Considerations

### 1. Traffic Gate Pattern
Keep the stabilization delay (4 seconds) before marking peers as stable. This prevents data loss on mixed 5G+WiFi networks.

### 2. Connection Retry Logic
Implement exponential backoff (1s, 2s, 4s) for connection retries, just like with MPC.

### 3. Swift 6 Concurrency
Use `guard let self else { return }` pattern before `Task` blocks to avoid Swift 6 warnings:

```swift
// Good pattern
connection.stateUpdateHandler = { [weak self] state in
    guard let self else { return }
    Task { @MainActor in
        self.handleState(state)
    }
}
```

### 4. Reconnection Handling
Add an `onHostConnected` callback to clear "disconnected" UI states when reconnecting:

```swift
var onHostConnected: (() -> Void)?

// In connection success handler:
isConnectedToHost = true
onHostConnected?()  // Clears "host lost" banners
```

### 5. Device Name in TXT Record
Network.framework doesn't expose device names like MPC's `MCPeerID.displayName`. Include it in the TXT record:

```swift
// Host: Add to TXT record
let txtEntry = "deviceName=\(UIDevice.current.name)"

// Guest: Extract from metadata
if case .bonjour(let txtRecord) = result.metadata {
    if let deviceName = txtRecord["deviceName"] {
        // Use deviceName
    }
}
```

---

## Migration Checklist

- [ ] Create `DiscoveredHost` and `ConnectedPeer` models
- [ ] Create `NetworkFraming` utility for TCP framing
- [ ] Create `P2PConnectionManager` with NWListener/NWBrowser
- [ ] (Optional) Create `BLEDiscoveryManager` for fast discovery
- [ ] Update Info.plist with BLE permissions (if using BLE)
- [ ] Update all views to use new manager
- [ ] Replace `MCPeerID` with `UUID` or custom struct
- [ ] Implement traffic gate pattern (4-second stabilization)
- [ ] Implement connection retry with exponential backoff
- [ ] Add `onHostConnected` callback for reconnection handling
- [ ] Test: Host starts, guest joins
- [ ] Test: Multiple guests join simultaneously
- [ ] Test: Guest leaves and rejoins
- [ ] Test: Host disconnects, guests receive notification
- [ ] Keep old `MultipeerManager` as fallback initially

---

## File Structure

```
YourApp/
├── Managers/
│   ├── MultipeerManager.swift      (keep as fallback)
│   ├── P2PConnectionManager.swift  (NEW - main networking)
│   ├── BLEDiscoveryManager.swift   (NEW - optional fast discovery)
│   └── NetworkFraming.swift        (NEW - TCP framing)
├── Models/
│   ├── DiscoveredHost.swift        (NEW)
│   └── ConnectedPeer.swift         (NEW)
```

---

## Full Mesh Network (Up to 8 Devices)

For apps where all devices need to communicate with each other (like a group chat or multiplayer game), you need a **full mesh topology** where every device connects to every other device.

### Mesh Network Architecture

```
     Device A
    /   |   \
   /    |    \
Device B----Device C
   \    |    /
    \   |   /
     Device D
```

In a full mesh with N devices, each device maintains N-1 connections.

### Key Differences from Host/Guest Model

| Host/Guest Model | Full Mesh Model |
|------------------|-----------------|
| One host, multiple guests | All peers are equal |
| Host runs NWListener only | Every device runs NWListener AND NWBrowser |
| Guests run NWBrowser only | Every device can initiate or accept connections |
| One-to-many communication | Many-to-many communication |

### MeshPeer Model

```swift
import Foundation

struct MeshPeer: Identifiable, Hashable {
    let id: String          // Unique device identifier (persisted across sessions)
    let displayName: String
    let discoveredAt: Date
    var isConnected: Bool
    var isStable: Bool

    init(id: String, displayName: String) {
        self.id = id
        self.displayName = displayName
        self.discoveredAt = Date()
        self.isConnected = false
        self.isStable = false
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    static func == (lhs: MeshPeer, rhs: MeshPeer) -> Bool {
        lhs.id == rhs.id
    }
}
```

### MeshNetworkManager

```swift
import Foundation
import Network
import UIKit
import Combine

@MainActor
class MeshNetworkManager: NSObject, ObservableObject {

    // MARK: - Published Properties

    @Published var discoveredPeers: [String: MeshPeer] = [:]
    @Published var connectedPeers: [String: MeshPeer] = [:]
    @Published var isRunning = false

    // MARK: - Callbacks

    var onMessageReceived: ((Data, MeshPeer) -> Void)?
    var onPeerConnected: ((MeshPeer) -> Void)?
    var onPeerDisconnected: ((MeshPeer) -> Void)?

    // MARK: - Private Properties

    private let serviceType = "_yourmesh._tcp"
    private let myPeerID: String  // Unique, persistent device ID
    private let myDisplayName: String

    private var listener: NWListener?
    private var browser: NWBrowser?
    private var connections: [String: NWConnection] = [:]  // peerID -> connection
    private var stablePeers: Set<String> = []
    private var pendingConnections: Set<String> = []  // Peers we're connecting to

    // MARK: - Initialization

    override init() {
        // Create or retrieve persistent device ID
        if let savedID = UserDefaults.standard.string(forKey: "meshPeerID") {
            self.myPeerID = savedID
        } else {
            let newID = UUID().uuidString
            UserDefaults.standard.set(newID, forKey: "meshPeerID")
            self.myPeerID = newID
        }
        self.myDisplayName = UIDevice.current.name
        super.init()
    }

    // MARK: - Network Parameters

    private func createParameters() -> NWParameters {
        let parameters = NWParameters.tcp
        parameters.includePeerToPeer = true
        parameters.serviceClass = .responsiveData
        return parameters
    }

    // MARK: - Start/Stop Mesh

    func startMesh() {
        guard !isRunning else { return }

        startListener()
        startBrowser()
        isRunning = true

        print("Mesh started with ID: \(myPeerID)")
    }

    func stopMesh() {
        listener?.cancel()
        listener = nil

        browser?.cancel()
        browser = nil

        for (_, connection) in connections {
            connection.cancel()
        }
        connections.removeAll()
        connectedPeers.removeAll()
        discoveredPeers.removeAll()
        stablePeers.removeAll()
        pendingConnections.removeAll()

        isRunning = false
    }

    // MARK: - Listener (Accept Incoming Connections)

    private func startListener() {
        do {
            let parameters = createParameters()
            listener = try NWListener(using: parameters)

            // Advertise with our peer ID and display name in TXT record
            let txtEntries = "peerID=\(myPeerID)\tdisplayName=\(myDisplayName)"
            var txtData = Data()
            for entry in txtEntries.split(separator: "\t") {
                let entryString = String(entry)
                txtData.append(UInt8(entryString.utf8.count))
                txtData.append(contentsOf: entryString.utf8)
            }

            listener?.service = NWListener.Service(
                name: myPeerID,  // Use peer ID as service name for uniqueness
                type: serviceType,
                txtRecord: txtData
            )

            listener?.stateUpdateHandler = { [weak self] state in
                guard let self else { return }
                Task { @MainActor in
                    if case .failed(let error) = state {
                        print("Listener failed: \(error)")
                    }
                }
            }

            listener?.newConnectionHandler = { [weak self] connection in
                guard let self else { return }
                Task { @MainActor in
                    self.handleIncomingConnection(connection)
                }
            }

            listener?.start(queue: .main)
        } catch {
            print("Failed to start listener: \(error)")
        }
    }

    // MARK: - Browser (Discover Other Peers)

    private func startBrowser() {
        let parameters = createParameters()
        browser = NWBrowser(for: .bonjour(type: serviceType, domain: nil), using: parameters)

        browser?.browseResultsChangedHandler = { [weak self] results, changes in
            guard let self else { return }
            Task { @MainActor in
                self.handleBrowseResults(changes: changes)
            }
        }

        browser?.start(queue: .main)
    }

    private func handleBrowseResults(changes: Set<NWBrowser.Result.Change>) {
        for change in changes {
            switch change {
            case .added(let result):
                handlePeerDiscovered(result)
            case .removed(let result):
                handlePeerLost(result)
            default:
                break
            }
        }
    }

    private func handlePeerDiscovered(_ result: NWBrowser.Result) {
        // Extract peer info from TXT record
        var peerID: String?
        var displayName = "Unknown"

        if case .bonjour(let txtRecord) = result.metadata {
            peerID = txtRecord["peerID"]
            displayName = txtRecord["displayName"] ?? "Unknown"
        }

        guard let peerID = peerID, peerID != myPeerID else { return }  // Ignore self

        // Already connected?
        guard connections[peerID] == nil else { return }

        let peer = MeshPeer(id: peerID, displayName: displayName)
        discoveredPeers[peerID] = peer

        print("Discovered peer: \(displayName) (\(peerID))")

        // Decide who initiates connection using tie-breaker
        // Lower peer ID initiates to avoid duplicate connections
        if myPeerID < peerID && !pendingConnections.contains(peerID) {
            initiateConnection(to: peer, result: result)
        }
    }

    private func handlePeerLost(_ result: NWBrowser.Result) {
        if case .service(let name, _, _, _) = result.endpoint {
            // Service name is the peer ID
            discoveredPeers.removeValue(forKey: name)
        }
    }

    // MARK: - Connection Management

    /// Initiate outgoing connection (we have lower ID)
    private func initiateConnection(to peer: MeshPeer, result: NWBrowser.Result) {
        pendingConnections.insert(peer.id)

        let parameters = createParameters()
        let connection = NWConnection(to: result.endpoint, using: parameters)

        connection.stateUpdateHandler = { [weak self] state in
            guard let self else { return }
            Task { @MainActor in
                self.handleConnectionState(state, peerID: peer.id, isOutgoing: true)
            }
        }

        connection.start(queue: .main)
        connections[peer.id] = connection

        print("Initiating connection to: \(peer.displayName)")
    }

    /// Handle incoming connection (other peer had lower ID)
    private func handleIncomingConnection(_ connection: NWConnection) {
        // We need to identify who this connection is from
        // Send our peer ID immediately after connection is ready
        connection.stateUpdateHandler = { [weak self] state in
            guard let self else { return }
            Task { @MainActor in
                self.handleIncomingConnectionState(state, connection: connection)
            }
        }

        connection.start(queue: .main)
    }

    private func handleIncomingConnectionState(_ state: NWConnection.State, connection: NWConnection) {
        switch state {
        case .ready:
            // Wait for the remote peer to identify themselves
            receiveIdentification(from: connection)
        case .failed, .cancelled:
            connection.cancel()
        default:
            break
        }
    }

    private func receiveIdentification(from connection: NWConnection) {
        NetworkFraming.receive(from: connection) { [weak self] result in
            guard let self else { return }
            Task { @MainActor in
                switch result {
                case .success(let data):
                    if let message = try? JSONDecoder().decode(MeshMessage.self, from: data),
                       case .identification(let peerID, let displayName) = message.type {
                        self.finalizeIncomingConnection(connection, peerID: peerID, displayName: displayName)
                    }
                case .failure(let error):
                    print("Failed to receive identification: \(error)")
                    connection.cancel()
                }
            }
        }
    }

    private func finalizeIncomingConnection(_ connection: NWConnection, peerID: String, displayName: String) {
        // Check if we already have a connection to this peer
        if let existing = connections[peerID] {
            // Keep the connection from the lower ID peer
            if myPeerID < peerID {
                // We should have initiated, reject this incoming
                connection.cancel()
                return
            } else {
                // They should have initiated, close our outgoing attempt
                existing.cancel()
            }
        }

        connections[peerID] = connection

        var peer = MeshPeer(id: peerID, displayName: displayName)
        peer.isConnected = true
        connectedPeers[peerID] = peer
        discoveredPeers.removeValue(forKey: peerID)

        // Start receiving messages
        startReceiving(from: connection, peerID: peerID)

        // Start stabilization
        startStabilization(for: peerID)

        print("Incoming connection from: \(displayName)")
    }

    private func handleConnectionState(_ state: NWConnection.State, peerID: String, isOutgoing: Bool) {
        switch state {
        case .ready:
            if isOutgoing {
                // Send identification
                sendIdentification(to: peerID)

                var peer = discoveredPeers[peerID] ?? MeshPeer(id: peerID, displayName: "Unknown")
                peer.isConnected = true
                connectedPeers[peerID] = peer
                discoveredPeers.removeValue(forKey: peerID)
                pendingConnections.remove(peerID)

                // Start receiving
                if let connection = connections[peerID] {
                    startReceiving(from: connection, peerID: peerID)
                }

                // Start stabilization
                startStabilization(for: peerID)

                print("Connected to: \(peer.displayName)")
            }

        case .failed(let error):
            print("Connection failed to \(peerID): \(error)")
            removeConnection(peerID: peerID)

        case .cancelled:
            removeConnection(peerID: peerID)

        default:
            break
        }
    }

    private func sendIdentification(to peerID: String) {
        guard let connection = connections[peerID] else { return }

        let message = MeshMessage(
            type: .identification(peerID: myPeerID, displayName: myDisplayName),
            senderID: myPeerID
        )

        if let data = try? JSONEncoder().encode(message) {
            NetworkFraming.send(data, over: connection) { error in
                if let error = error {
                    print("Failed to send identification: \(error)")
                }
            }
        }
    }

    private func startReceiving(from connection: NWConnection, peerID: String) {
        NetworkFraming.startReceiving(from: connection) { [weak self] data in
            guard let self else { return }
            Task { @MainActor in
                self.handleReceivedData(data, from: peerID)
            }
        } onError: { [weak self] error in
            guard let self else { return }
            Task { @MainActor in
                print("Receive error from \(peerID): \(error)")
                self.removeConnection(peerID: peerID)
            }
        }
    }

    private func startStabilization(for peerID: String) {
        Task { @MainActor in
            try? await Task.sleep(nanoseconds: 4_000_000_000)  // 4 seconds

            guard connectedPeers[peerID] != nil else { return }

            stablePeers.insert(peerID)
            connectedPeers[peerID]?.isStable = true

            if let peer = connectedPeers[peerID] {
                onPeerConnected?(peer)
            }

            print("Peer stabilized: \(peerID)")
        }
    }

    private func removeConnection(peerID: String) {
        connections[peerID]?.cancel()
        connections.removeValue(forKey: peerID)
        pendingConnections.remove(peerID)
        stablePeers.remove(peerID)

        if let peer = connectedPeers.removeValue(forKey: peerID) {
            onPeerDisconnected?(peer)
        }
    }

    // MARK: - Message Handling

    private func handleReceivedData(_ data: Data, from peerID: String) {
        guard let message = try? JSONDecoder().decode(MeshMessage.self, from: data) else {
            return
        }

        switch message.type {
        case .identification:
            // Already handled during connection setup
            break

        case .broadcast(let payload):
            if let peer = connectedPeers[peerID] {
                onMessageReceived?(payload, peer)
            }

        case .direct(let payload):
            if let peer = connectedPeers[peerID] {
                onMessageReceived?(payload, peer)
            }
        }
    }

    // MARK: - Sending Messages

    /// Send to all connected peers
    func broadcast(_ data: Data) {
        let message = MeshMessage(type: .broadcast(payload: data), senderID: myPeerID)
        guard let encoded = try? JSONEncoder().encode(message) else { return }

        for peerID in stablePeers {
            if let connection = connections[peerID] {
                NetworkFraming.send(encoded, over: connection) { error in
                    if let error = error {
                        print("Broadcast failed to \(peerID): \(error)")
                    }
                }
            }
        }
    }

    /// Send to specific peer
    func send(_ data: Data, to peerID: String) {
        guard stablePeers.contains(peerID),
              let connection = connections[peerID] else { return }

        let message = MeshMessage(type: .direct(payload: data), senderID: myPeerID)
        guard let encoded = try? JSONEncoder().encode(message) else { return }

        NetworkFraming.send(encoded, over: connection) { error in
            if let error = error {
                print("Send failed to \(peerID): \(error)")
            }
        }
    }

    /// Send to all except specified peers
    func broadcast(_ data: Data, excluding: Set<String>) {
        let message = MeshMessage(type: .broadcast(payload: data), senderID: myPeerID)
        guard let encoded = try? JSONEncoder().encode(message) else { return }

        for peerID in stablePeers where !excluding.contains(peerID) {
            if let connection = connections[peerID] {
                NetworkFraming.send(encoded, over: connection) { error in
                    if let error = error {
                        print("Broadcast failed to \(peerID): \(error)")
                    }
                }
            }
        }
    }
}
```

### MeshMessage Protocol

```swift
import Foundation

struct MeshMessage: Codable {
    enum MessageType: Codable {
        case identification(peerID: String, displayName: String)
        case broadcast(payload: Data)
        case direct(payload: Data)
    }

    let type: MessageType
    let senderID: String
    let timestamp: Date

    init(type: MessageType, senderID: String) {
        self.type = type
        self.senderID = senderID
        self.timestamp = Date()
    }
}
```

### Usage Example

```swift
struct MeshChatView: View {
    @StateObject private var mesh = MeshNetworkManager()
    @State private var messages: [(String, MeshPeer)] = []
    @State private var inputText = ""

    var body: some View {
        VStack {
            // Connected peers
            HStack {
                Text("Connected: \(mesh.connectedPeers.count)")
                ForEach(Array(mesh.connectedPeers.values)) { peer in
                    Text(peer.displayName)
                        .padding(4)
                        .background(peer.isStable ? Color.green : Color.orange)
                        .cornerRadius(4)
                }
            }

            // Messages
            List(messages, id: \.0) { message, peer in
                Text("\(peer.displayName): \(message)")
            }

            // Input
            HStack {
                TextField("Message", text: $inputText)
                Button("Send") {
                    if let data = inputText.data(using: .utf8) {
                        mesh.broadcast(data)
                        inputText = ""
                    }
                }
            }
        }
        .onAppear {
            mesh.onMessageReceived = { data, peer in
                if let text = String(data: data, encoding: .utf8) {
                    messages.append((text, peer))
                }
            }
            mesh.startMesh()
        }
        .onDisappear {
            mesh.stopMesh()
        }
    }
}
```

### Connection Tie-Breaker Strategy

To avoid duplicate connections (both peers trying to connect to each other), use a **deterministic tie-breaker**:

```swift
// Lower peer ID initiates the connection
if myPeerID < discoveredPeerID {
    initiateConnection(to: peer)
} else {
    // Wait for them to connect to us
}
```

This ensures:
- Only ONE connection is created between any two peers
- No race conditions or duplicate connections
- Deterministic behavior across all devices

### Mesh Network Limits

| Devices | Connections per Device | Total Connections |
|---------|----------------------|-------------------|
| 2       | 1                    | 1                 |
| 3       | 2                    | 3                 |
| 4       | 3                    | 6                 |
| 5       | 4                    | 10                |
| 6       | 5                    | 15                |
| 7       | 6                    | 21                |
| 8       | 7                    | 28                |

Formula: Total connections = N × (N-1) / 2

### Mesh Network Considerations

1. **Connection Overhead**: Each device maintains N-1 connections. With 8 devices, that's 7 connections per device and 28 total connections in the network.

2. **Message Duplication**: When broadcasting, each peer sends to all others. Ensure your app handles potential duplicate messages (use message IDs).

3. **Partial Mesh**: For larger groups, consider a **partial mesh** where not everyone connects to everyone. Use relay nodes instead.

4. **Battery Impact**: More connections = more battery usage. Consider reducing broadcast frequency or implementing sleep modes.

5. **Network Partition**: Handle cases where the mesh splits (some devices can't reach others). Implement reconnection logic.

6. **Message Ordering**: Messages may arrive out of order. Use timestamps or sequence numbers if order matters.

### Message Relay (Optional)

For messages that need to reach peers you're not directly connected to:

```swift
func relayBroadcast(_ data: Data, originalSenderID: String, seenBy: Set<String>) {
    var updatedSeenBy = seenBy
    updatedSeenBy.insert(myPeerID)

    let message = MeshMessage(
        type: .relayedBroadcast(payload: data, originalSender: originalSenderID, seenBy: updatedSeenBy),
        senderID: myPeerID
    )

    guard let encoded = try? JSONEncoder().encode(message) else { return }

    // Forward to peers who haven't seen it
    for peerID in stablePeers where !updatedSeenBy.contains(peerID) {
        if let connection = connections[peerID] {
            NetworkFraming.send(encoded, over: connection) { _ in }
        }
    }
}
```

This allows messages to propagate through the entire mesh even if not all devices are directly connected.
