import MultipeerConnectivity
import SwiftUI
import Combine
import os

class MultipeerSession: NSObject, ObservableObject {
    private let serviceType = Constants.serviceType
    
    // Services
    private var myPeerId: MCPeerID? // Made optional to prevent cleanup/restart crashes
    private var serviceAdvertiser: MCNearbyServiceAdvertiser!
    private var serviceBrowser: MCNearbyServiceBrowser!
    private var session: MCSession? // Already optional? No, it was ! before. Let's check.
    private var myDiscoveryInfo: [String: String]? // Store my info for tie-breaking
    private var isRestarting = false // Debounce flag
    private var isStarted = false // Debounce flag to prevent duplicate start() calls
    private var delayedRestartItem: DispatchWorkItem? // Item for delayed recovery logic
    private var keepAliveTimer: Timer? // Heartbeat to maintain AWDL links
    private let log = Logger()
    
    // Node Identity (stable per install + monotonic instance per session)
    private var myNodeID: String = ""
    private var myInstance: Int = 0
    
    // Per-peer state for robust mesh handling (keyed by nodeID)
    private var latestByNodeID: [String: (peerID: MCPeerID, instance: Int)] = [:]
    private var pendingInvites: [String: MCPeerID] = [:]  // nodeID -> peerID for active invites (enables cancellation)
    private var cooldownUntil: [String: Date] = [:]  // nodeIDs to temporarily ignore
    private var connectingTimeoutTimers: [MCPeerID: DispatchWorkItem] = [:]  // Per-peer watchdogs
    private var stabilityTimers: [String: Timer] = [:] // nodeID -> Timer (15s reset delay)
    private var peerNodeIDMap: [MCPeerID: String] = [:]  // Map MCPeerID -> nodeID for lookups
    
    // Poisoned state detection
    private var consecutiveFailures: [String: Int] = [:]  // Track failures per nodeID
    private static let poisonedThreshold = 5  // N consecutive failures = poisoned (higher for AWDL flakiness)
    
    // State
    @Published var connectedPeers: [MCPeerID] = []
    @Published var connectedPeerUsers: [PeerUser] = []
    @Published var receivedMessages: [MeshMessage] = []
    @Published var liveTranscripts: [String: String] = [:] // Keyed by senderNodeID for reliable identity
    
    // Mesh Capacity Limit (MPC is unstable beyond 7-8 peers)
    static let maxPeers = 7
    var isAtCapacity: Bool { connectedPeers.count >= Self.maxPeers }
    
    // Expose nodeID for UI to determine message ownership
    var myNodeIDPublic: String { myNodeID }
    var myInstancePublic: Int { myInstance }
    
    // User Details Wrappers
    var userName: String { myUserName }
    var userColor: String { myUserColor }
    var userRole: UserRole { myUserRole }
    
    // User Details
    private var myUserName: String = UIDevice.current.name
    private var myUserColor: String = "#3357FF"
    private var myUserRole: UserRole = .hearing
    
    // Dynamic Identity Update (since Session persists longer than Onboarding)
    func setIdentity(name: String, colorHex: String, role: UserRole) {
        self.myUserName = name
        self.myUserColor = colorHex
        self.myUserRole = role
        // Note: Changing identity doesn't restart services automatically
        // but new connections will see updated info.
        // For deeper updates, we'd need to re-create peerID, but simpler is better here.
    }
    
    override init() {
        super.init()
        ensureIdentity()
        // Services now started manually via start() to support app-level lifecycle
    }
    
    // MARK: - Lifecycle Control
    
    func start() {
        // Ensure session is set up (fresh session on every start)
        if session == nil {
            setupSession()
        }
        startServices()
    }
    
    func stop() {
        // Reuse the robust disconnect logic to ensure all state/UI is cleared
        disconnect()
    }
    

    
    private func ensureIdentity() {
        // Generate/retrieve stable identity ONCE per app launch
        // OR recreate if myPeerId was wiped (e.g. after disconnect/leave)
        if myNodeID.isEmpty {
            myNodeID = NodeIdentity.nodeID
            myInstance = NodeIdentity.nextInstance()
            
            // Create fresh ID (but keep displayName stable for this app run if possible)
            let peerID = MCPeerID(displayName: myUserName)
            self.myPeerId = peerID
        } else if myPeerId == nil {
            // RECOVERY: NodeID exists but PeerID is gone (disconnect called). Recreate it.
            log.info("Identity Recovery: Recreating MCPeerID for existing NodeID \(self.myNodeID)")
            let peerID = MCPeerID(displayName: myUserName)
            self.myPeerId = peerID
        }
    }
    
    private func setupSession() {
        guard let peerID = self.myPeerId else {
            log.error("Attempted to setup session without identity!")
            ensureIdentity()
            // CRITICAL FIX: After identity recovery, we MUST complete the setup.
            // Recursively call setupSession now that myPeerId exists.
            setupSession()
            return
        }
         
        // Create fresh Session
        // Encryption .none is recommended for reliable AWDL (mixed network) connections to avoid handshake timeouts
        session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .none)
        session?.delegate = self
        
        // Create fresh Advertiser & Browser with STABLE NODE IDENTITY
        // nodeID is stable per install, instance increments per session start
        // This allows other devices to reliably identify us and filter ghost peers
        let discoveryInfo: [String: String] = [
            "nodeID": myNodeID,
            "instance": String(myInstance),
            "ts": String(Date().timeIntervalSince1970)
        ]
        self.myDiscoveryInfo = discoveryInfo
        
        serviceAdvertiser = MCNearbyServiceAdvertiser(
            peer: peerID,
            discoveryInfo: discoveryInfo,
            serviceType: serviceType
        )
        serviceAdvertiser.delegate = self
        
        serviceBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
        serviceBrowser.delegate = self
        
        log.info("Session setup complete with nodeID: \(self.myNodeID) instance: \(self.myInstance) peerID: \(peerID.displayName)")
    }
    
    private func startServices() {
        guard serviceAdvertiser != nil, serviceBrowser != nil else { return }
        serviceBrowser.startBrowsingForPeers()
        
        // Delay advertising to ensure MCSession listener is ready
        // Reduced to 0.5s (was 2.0s) - 2.0s was causing "Connection Refused" because 
        // peers tried to connect before we were ready. 0.5s is sufficient for cleanup.
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            guard let self = self else { return }
            self.serviceAdvertiser?.startAdvertisingPeer()
            self.log.info("Advertising started (delayed 0.5s for listener warmup)")
        }
    }
    
    private func stopServices() {
        serviceAdvertiser?.stopAdvertisingPeer()
        serviceBrowser?.stopBrowsingForPeers()
    }
    
    // Helper to refresh service state without killing connections
    // We restart BOTH Advertiser and Browser to flush stale routes/caches in the OS/AWDL stack
    private func restartServices(forcePoisonedRecovery: Bool = false) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            
            // Debounce: If already restarting, ignore this request
            guard !self.isRestarting else {
                self.log.info("Services already restarting, skipping request.")
                return
            }
            
            // Cancel any pending delayed restart since we are restarting now
            self.delayedRestartItem?.cancel()
            self.delayedRestartItem = nil
            
            // Cancel all per-peer timers
            for (_, timer) in self.connectingTimeoutTimers {
                timer.cancel()
            }
            self.connectingTimeoutTimers.removeAll()
            
            // Stop keep-alive to prevent multiple timers if restart happens while peers present
            self.stopKeepAlive()
            
            self.isRestarting = true
            
            // CLEAR PER-PEER STATE: Remove stale peer info from previous sessions
            self.latestByNodeID.removeAll()
            self.pendingInvites.removeAll()
            self.peerNodeIDMap.removeAll()
            // Keep cooldownUntil - we want to remember temporarily-banned peers
            // Keep consecutiveFailures - we need this history for poisoned state detection
            
            self.log.info("Restarting services to flush stale peers/routes...")
            self.serviceBrowser?.stopBrowsingForPeers()
            self.serviceAdvertiser?.stopAdvertisingPeer()
            
            // Add RANDOM JITTER to break synchronized restart loops
            let jitter = Double.random(in: 0.0...1.0)
            // OPTIMIZATION: Faster restart on Wi-Fi (1.0s base) vs AWDL (1.0s base)
            // Increased to 1.0s to ensure socket cleanup and prevent "Connection refused" loops
            let baseDelay = 1.0
            let totalDelay = baseDelay + jitter
            self.log.info("Restart delay: \(String(format: "%.2f", totalDelay))s (with jitter)")
            
            DispatchQueue.main.asyncAfter(deadline: .now() + totalDelay) { [weak self] in
                guard let self = self else { return }
                
                // Clear delegate BEFORE disconnecting to prevent zombie callbacks
                self.session?.delegate = nil
                self.session?.disconnect()
                self.session = nil
                
                // POISONED STATE: Only rebuild MCPeerID if in poisoned state
                // Otherwise keep same PeerID to reduce ghost artifacts on other devices
                if forcePoisonedRecovery {
                    self.log.info("POISONED STATE detected. Full reset with new MCPeerID.")
                    self.consecutiveFailures.removeAll() // Reset failure tracking
                    self.cooldownUntil.removeAll() // Reset cooldowns
                    self.setupSession() // New MCPeerID + new instance
                } else {
                    // Keep MCPeerID stable, just increment instance and rebuild session
                    self.myInstance = NodeIdentity.nextInstance()
                    
                    let discoveryInfo: [String: String] = [
                        "nodeID": self.myNodeID,
                        "instance": String(self.myInstance),
                        "ts": String(Date().timeIntervalSince1970)
                    ]
                    self.myDiscoveryInfo = discoveryInfo
                    
                    // Recreate session with same PeerID
                    guard let peerID = self.myPeerId else {
                        self.log.error("Restart failed: myPeerId is nil. Re-running setup.")
                        self.setupSession() // Fallback to full setup
                        return
                    }
                    
                    self.session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .none)
                    self.session?.delegate = self
                    
                    // Recreate advertiser/browser with new discovery info
                    self.serviceAdvertiser = MCNearbyServiceAdvertiser(
                        peer: peerID,
                        discoveryInfo: discoveryInfo,
                        serviceType: self.serviceType
                    )
                    self.serviceAdvertiser.delegate = self
                    
                    self.serviceBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: self.serviceType)
                    self.serviceBrowser.delegate = self
                    
                    self.log.info("Services rebuilt with same PeerID, new instance: \(self.myInstance)")
                }
                
                self.serviceBrowser?.startBrowsingForPeers()
                
                // 0.5s delay to ensure old listener is fully unbound
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
                guard let self = self else { return }
                guard self.isRestarting else { return } // Abort if cancelled
                
                self.serviceAdvertiser?.startAdvertisingPeer()
                self.log.info("Advertising restarted (delayed 0.5s)")
                
                self.isRestarting = false
            }
            }
        }
    }
    
    func updateMyDetails(name: String, color: String, role: UserRole) {
        self.myUserName = name
        self.myUserColor = color
        self.myUserRole = role
        
        // If connected, resend handshake with new details
        if let session = session, !session.connectedPeers.isEmpty {
            sendHandshake()
        }
    }
    
    func disconnect() {
        // 1. Cancel any pending recovery task immediately
        self.delayedRestartItem?.cancel()
        self.delayedRestartItem = nil
        
        // 2. Cancel all per-peer timers to prevent zombie callbacks
        for (_, timer) in connectingTimeoutTimers {
            timer.cancel()
        }
        connectingTimeoutTimers.removeAll()
        
        // Stop Keep-Alive heartbeat
        stopKeepAlive()
        
        stopServices()
        serviceAdvertiser?.delegate = nil
        serviceBrowser?.delegate = nil
        session?.delegate = nil
        session?.disconnect()
        
        // Clear references
        serviceAdvertiser = nil
        serviceBrowser = nil
        session = nil
        myPeerId = nil
        
        // 3. Clear per-peer state
        pendingInvites.removeAll()
        latestByNodeID.removeAll()
        peerNodeIDMap.removeAll()
        cooldownUntil.removeAll()
        consecutiveFailures.removeAll()
        
        // 4. Reset flags
        isStarted = false
        isRestarting = false
        log.info("Services stopped, all timers cancelled, per-peer state cleared.")
        
        DispatchQueue.main.async {
            self.connectedPeers.removeAll()
            self.connectedPeerUsers.removeAll()
            self.receivedMessages.removeAll()
            self.liveTranscripts.removeAll()
        }
    }
    
    func send(message: MeshMessage) {
        guard let session = session, !session.connectedPeers.isEmpty else { return }
        
        // Enforce senderNodeID on all outgoing messages
        var msg = message
        if msg.senderNodeID.isEmpty {
            msg.senderNodeID = myNodeID
        }
        
        do {
            let data = try JSONEncoder().encode(msg)
            // Use unreliable delivery for partials to reduce overhead/latency
            let mode: MCSessionSendDataMode = msg.isPartial ? .unreliable : .reliable
            try session.send(data, toPeers: session.connectedPeers, with: mode)
            
            // Handle local state updates
            DispatchQueue.main.async {
                if msg.isHandshake {
                    // Don't show handshake in messages
                } else if msg.isPartial {
                    // For partials, we don't usually show our own in the live row
                } else if msg.isKeepAlive {
                    // Don't show keep-alive heartbeats in messages
                } else {
                    self.receivedMessages.append(msg)
                }
            }
        } catch {
            log.error("Error sending message: \(error.localizedDescription)")
        }
    }
    
    private func sendHandshake() {
        let message = MeshMessage(
            id: UUID(),
            senderID: myUserName,
            senderNodeID: myNodeID,
            senderInstance: myInstance, // CRITICAL: Authoritative instance for ghost filtering
            senderRole: myUserRole,
            senderColorHex: myUserColor,
            content: "Handshake",
            isTranscription: false,
            isPartial: false,
            isHandshake: true,
            timestamp: Date()
        )
        send(message: message)
    }
    
    // MARK: - Keep-Alive (Maintain AWDL Links)
    
    private func startKeepAlive() {
        stopKeepAlive() // Clear any existing timer
        keepAliveTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in
            self?.sendKeepAlive()
        }
        log.info("Keep-Alive timer started (10s interval).")
    }
    
    private func stopKeepAlive() {
        keepAliveTimer?.invalidate()
        keepAliveTimer = nil
    }
    
    private func sendKeepAlive() {
        guard let session = session, !session.connectedPeers.isEmpty else { return }
        let message = MeshMessage(
            id: UUID(),
            senderID: myUserName,
            senderNodeID: myNodeID,
            senderRole: myUserRole,
            senderColorHex: myUserColor,
            content: "💓",
            isTranscription: false,
            isPartial: false,
            isHandshake: false,
            isKeepAlive: true,
            connectedNodeIDs: self.connectedPeerUsers.map { $0.nodeID }.filter { !$0.isEmpty }, // Gossip: Share who we know
            timestamp: Date()
        )
        send(message: message)
    }
    
    // MARK: - Helper Methods
    
    /// Calculate exponential backoff for a peer based on failure count
    /// 3s → 6s → 12s → max 30s
    private func calculateBackoff(failures: Int) -> TimeInterval {
        // Base = 0.5s (was 2.0s). Max = 30s.
        // 1: 0.5s
        // 2: 1.0s
        // 3: 2.0s
        // 4: 4.0s
        // ...
        let baseDelay: TimeInterval = 0.5
        let exponential = pow(2.0, Double(failures - 1))
        let delay = baseDelay * exponential
        return min(delay, 30.0)
    }
    
    private func createInviteContext() -> Data? {
        return try? JSONEncoder().encode([
            "nodeID": self.myNodeID,
            "instance": String(self.myInstance)
        ])
    }
    
    /// Schedule a restart only if the mesh stays empty for 2 seconds
    private func scheduleRestartIfStillEmpty() {
        delayedRestartItem?.cancel()
        let item = DispatchWorkItem { [weak self] in
            guard let self = self else { return }
            guard self.connectedPeers.isEmpty else {
                self.log.info("Skipping restart - we now have connections.")
                return
            }
            // NO-OP: We disabled the aggressive restart because it causes infinite loops ("Death Spiral")
            // when one peer is stuck in a bad state. It causes Instance ID cycling which confuses other peers.
            // We now rely purely on "Smart Retry" to reconnect us.
            self.log.info("Mesh still empty. Skipping delayed restart to preserve stability (Smart Retry handles reconnection).")
            // self.restartServices() // DISABLED
        }
        delayedRestartItem = item
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: item)
    }
    
    deinit {
        disconnect()
    }
}

extension MultipeerSession: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
        log.error("ServiceAdvertiser didNotStartAdvertisingPeer: \(error.localizedDescription)")
    }
    
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            // IDENTITY CHECK: Ignore stale advertisers
            guard advertiser == self.serviceAdvertiser else { return }
            
            self.log.info("didReceiveInvitationFromPeer \(peerID)")
            
            // Extract nodeID from invitation context (if provided by inviter)
            if let context = context,
               let contextDict = try? JSONDecoder().decode([String: String].self, from: context),
               let inviterNodeID = contextDict["nodeID"],
               let inviterInstanceStr = contextDict["instance"],
               let inviterInstance = Int(inviterInstanceStr) {
                // Map peerID -> nodeID immediately
                self.peerNodeIDMap[peerID] = inviterNodeID
                
                // Ghost filtering: reject invitations from older instances
                if let existing = self.latestByNodeID[inviterNodeID] {
                    if inviterInstance > existing.instance {
                        self.latestByNodeID[inviterNodeID] = (peerID, inviterInstance)
                        self.log.info("Invitation context: updated to newer instance \(inviterInstance) for \(inviterNodeID)")
                    } else if inviterInstance < existing.instance {
                        // REJECT GHOST INVITATION: We've seen a newer instance via discovery
                        self.log.info("Rejecting GHOST invitation from \(inviterNodeID) instance \(inviterInstance) (we know instance \(existing.instance))")
                        invitationHandler(false, nil)
                        return
                    } else {
                        // Same instance - could be same peer or MCPeerID mismatch, accept it
                        self.log.info("Invitation from same instance \(inviterInstance) for \(inviterNodeID)")
                    }
                } else {
                    self.latestByNodeID[inviterNodeID] = (peerID, inviterInstance)
                    self.log.info("Extracted inviter nodeID: \(inviterNodeID) instance: \(inviterInstance)")
                }
            }
            
            // DEADLOCK FIX: If they are inviting us, they think we are NOT connected.
            // If we think we ARE connected, we are wrong (stale/half-open).
            // We must Disconnect our stale socket and Accept the new invite.
            if self.session?.connectedPeers.contains(peerID) == true {
                 self.log.warning("Received invitation from 'connected' peer \(peerID). Assuming Half-Open Deadlock. Resetting connection...")
                 // Kill the stale connection
                 self.session?.cancelConnectPeer(peerID)
                 
                 // DELAY ACCEPTANCE to allow disconnect to propagate and clear state (0.5s)
                 // This prevents "Connection refused" or immediate closure of the new socket
                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
                     guard let self = self else { return }
                     guard self.session != nil else { return }
                     self.log.info("Accepting invitation after Half-Open reset delay.")
                     invitationHandler(true, self.session)
                 }
                 return
            }
            
            // CAPACITY CHECK: Reject new peers if mesh is at capacity
            guard !self.isAtCapacity else {
                self.log.info("Mesh at capacity (\(Self.maxPeers) peers). Rejecting invitation from \(peerID).")
                invitationHandler(false, nil)
                return
            }
            
            // IMMEDIATE ACCEPT: Per stability protocol, accept with 0.0s delay.
            guard self.session != nil else { return }
            invitationHandler(true, self.session)
        }
    }
}

extension MultipeerSession: MCNearbyServiceBrowserDelegate {
    func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
        log.error("ServiceBrowser didNotStartBrowsingForPeers: \(error.localizedDescription)")
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            // IDENTITY CHECK: Ignore stale browsers
            guard browser == self.serviceBrowser else { return }
            
            self.log.info("ServiceBrowser foundPeer: \(peerID)")
            
            guard let session = self.session else { return }
            
            // If already connected, ignore
            if session.connectedPeers.contains(peerID) {
                return
            }
            
            // REQUIRE nodeID in discovery info (no backward compatibility needed)
            guard let theirNodeID = info?["nodeID"],
                  let theirInstanceStr = info?["instance"],
                  let theirInstance = Int(theirInstanceStr) else {
                self.log.info("Ignoring peer without nodeID/instance: \(peerID)")
                return
            }
            
            self.log.info("Found peer \(peerID) nodeID: \(theirNodeID) instance: \(theirInstance)")
            
            // Store nodeID -> peerID mapping for later lookups
            self.peerNodeIDMap[peerID] = theirNodeID
            
            // 1. GHOST FILTERING: Keep only the highest instance for each nodeID
            //    Do this FIRST so we can clear cooldown on restart
            if let existing = self.latestByNodeID[theirNodeID] {
                if theirInstance > existing.instance {
                    // GHOST BUSTERS: Aggressively kill connection to the old stale peerID
                    self.session?.cancelConnectPeer(existing.peerID)
                    self.log.info("Found NEWER instance for nodeID \(theirNodeID): \(theirInstance) > \(existing.instance). Cancelled connection to ghost \(existing.peerID).")

                    self.latestByNodeID[theirNodeID] = (peerID, theirInstance)
                    
                    // CLEAR COOLDOWN: Peer restarted, give them another chance
                    if self.cooldownUntil[theirNodeID] != nil {
                        self.cooldownUntil.removeValue(forKey: theirNodeID)
                        self.consecutiveFailures.removeValue(forKey: theirNodeID)
                        self.log.info("Cleared cooldown for \(theirNodeID) due to new instance")
                    }
                    
                    // CLEAR PENDING INVITE: If we were inviting the old instance, cancel it so we can invite the new one
                    if let pendingPeerID = self.pendingInvites[theirNodeID] {
                        self.log.info("Cancelling stale pending invite to \(pendingPeerID) for newer instance")
                        // Note: We can't explicitly 'cancel' an invite in MPC, but we remove the tracking so we can send a new one
                        self.pendingInvites.removeValue(forKey: theirNodeID)
                        
                        // If we had a watchdog timer for the old peer, cancel it
                        self.connectingTimeoutTimers[pendingPeerID]?.cancel()
                        self.connectingTimeoutTimers.removeValue(forKey: pendingPeerID)
                    }
                } else if theirInstance < existing.instance {
                    self.log.info("Ignoring OLDER instance for nodeID \(theirNodeID): \(theirInstance) < \(existing.instance)")
                    return // Ignore ghost peer
                } else if existing.peerID != peerID {
                    // Same instance but different MCPeerID - likely a race condition, keep existing
                    self.log.info("Ignoring duplicate instance for nodeID \(theirNodeID)")
                    return
                }
            } else {
                self.latestByNodeID[theirNodeID] = (peerID, theirInstance)
            }
            
            // 2. COOLDOWN CHECK: Ignore recently-failed peers (after ghost filtering)
            if let cooldownEnd = self.cooldownUntil[theirNodeID], Date() < cooldownEnd {
                self.log.info("Ignoring peer \(theirNodeID) - still in cooldown until \(cooldownEnd)")
                return
            }
            
            // 3. DETERMINISTIC LEADER/FOLLOWER: Use nodeID comparison (simple and reliable)
            var shouldInvite = false
            
            if self.myNodeID > theirNodeID {
                shouldInvite = true
                self.log.info("Decision: I am LEADER (myNodeID > theirNodeID). I will invite.")
            } else if self.myNodeID < theirNodeID {
                shouldInvite = false
                self.log.info("Decision: I am FOLLOWER (myNodeID < theirNodeID). I will wait.")
            } else {
                // Same nodeID (shouldn't happen - same device) - compare instance
                shouldInvite = self.myInstance > theirInstance
                self.log.info("Decision: Same nodeID, comparing instance (\(self.myInstance) vs \(theirInstance)).")
            }
            
            if shouldInvite {
                // CAPACITY CHECK: Skip invite if mesh is at capacity
                guard !self.isAtCapacity else {
                    self.log.info("Mesh at capacity (\(Self.maxPeers) peers). Skipping invite to \(peerID).")
                    return
                }
                
                guard !self.isRestarting else { return }
                
                // PENDING INVITE CHECK: Don't double-invite
                guard self.pendingInvites[theirNodeID] == nil else {
                    self.log.info("Already have pending invite for nodeID \(theirNodeID). Skipping.")
                    return
                }
                
                // GHOST CHECK: Ensure this is still the latest peer for this nodeID
                if let latest = self.latestByNodeID[theirNodeID],
                   latest.peerID != peerID {
                    self.log.info("Aborting invite to \(peerID). Found newer MCPeerID for nodeID \(theirNodeID)!")
                    return
                }
                
                // Track pending invite with peerID for cancellation
                self.pendingInvites[theirNodeID] = peerID
                
                // Include nodeID/instance in invitation context so receiver can map immediately
                let contextData = try? JSONEncoder().encode([
                    "nodeID": self.myNodeID,
                    "instance": String(self.myInstance)
                ])
                
                // WARMUP DELAY: Wait 2.0s before inviting to allow listener to spin up
                // This prevents "Connection refused" if we discover them immediately after they restart
                self.log.info("Inviting peer \(peerID) (nodeID: \(theirNodeID)) in 1.0s (listener warmup).")
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
                    guard let self = self else { return }
                    
                    // VALIDATION CHECKS after delay
                    guard browser == self.serviceBrowser else {
                        self.log.info("Aborting delayed invite to \(peerID). Browser mismatch (restart occurred?).")
                        return
                    }
                    guard let session = self.session else { return }
                    
                    // 1. Is peer still pending? (Might have been cancelled by ghost check)
                    guard self.pendingInvites[theirNodeID] == peerID else {
                        self.log.info("Aborting delayed invite to \(peerID). No longer pending.")
                        return
                    }
                    
                    // 2. Are we already connected?
                    if session.connectedPeers.contains(peerID) {
                        return
                    }
                    
                    browser.invitePeer(peerID, to: session, withContext: contextData, timeout: 10) // Increased timeout slightly
                }
            }
        }
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self, browser == self.serviceBrowser else { return }
            self.log.info("ServiceBrowser lostPeer: \(peerID)")
            
            // Clean up per-peer state for lost peer
            if let nodeID = self.peerNodeIDMap[peerID] {
                // Remove pending invite if it was for this peer
                if self.pendingInvites[nodeID] == peerID {
                    self.pendingInvites.removeValue(forKey: nodeID)
                    self.log.info("Cleared pending invite for nodeID \(nodeID)")
                }
                
                // Clear latestByNodeID if it points to this peerID
                if let latest = self.latestByNodeID[nodeID], latest.peerID == peerID {
                    self.latestByNodeID.removeValue(forKey: nodeID)
                    self.log.info("Cleared latestByNodeID for nodeID \(nodeID)")
                }
            }
            
            // Remove peerNodeIDMap entry
            self.peerNodeIDMap.removeValue(forKey: peerID)
            

        }
    }
}

extension MultipeerSession: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            
            // DEBUG: Raw session logging to catch Zombies
            let sessionStatus = (session == self.session) ? "CURRENT" : "ZOMBIE"
            self.log.debug("RAW didChange: \(peerID) -> \(state.rawValue) on \(sessionStatus) session")
            
            // IDENTITY CHECK: Ignore stale sessions
            guard session == self.session else { return }
            
            // Look up nodeID for this peer
            let nodeID = self.peerNodeIDMap[peerID]
            
            self.log.info("peer \(peerID) (nodeID: \(nodeID ?? "unknown")) didChange state: \(state.rawValue)")
            
            switch state {
            case .connected:
                // Cancel per-peer watchdog timer
                // Cancel per-peer watchdog timer (if it exists for this specific socket)
                // Note: We used to cancel on Handshake, but we can also cancel here effectively IF we trust the socket state.
                // Actually, let's keep the logic consistent: Watchdog guards HANDSHAKE.
                // But if we are connected, we *might* want to give it a grace period?
                // No, sticking to "Cancel on Handshake" is safest.
                // However, we MUST ensure old timers for *this* peerID are cleaned up if we reconnect?
                
                 // Clean up previous timers for this socket if any (should be none as it's a new connection)
                 self.connectingTimeoutTimers[peerID]?.cancel()
                 self.connectingTimeoutTimers.removeValue(forKey: peerID)
                 
                 // NOTE: We do NOT reset consecutiveFailures here.
                 // We wait for Handshake + Stability Period.
                
                // Cancel any pending delayed restart (Recovery Succeeded!)
                self.delayedRestartItem?.cancel()
                self.delayedRestartItem = nil
                
                // Track previous peer count for KeepAlive lifecycle
                let previousPeerCount = self.connectedPeers.count
                
                // Update connectedPeers IMMEDIATELY for responsive UI and correct mesh-empty checks
                self.connectedPeers = session.connectedPeers
                
                // KeepAlive: Start only when transitioning from 0 → 1 peers
                if previousPeerCount == 0 && self.connectedPeers.count >= 1 {
                    self.startKeepAlive()
                }
                
                    // Traffic Gating
                    Task {
                        do {
                            // OPTIMIZATION: Check if we are on WiFi
                            // If on WiFi (stable), wait only 100ms.
                            // If on AWDL/Cellular (unstable), wait the full 1.0s to let the mesh settle.
                            let isWiFi = NetworkMonitor.shared.isWiFi
                            let delay: UInt64 = isWiFi ? 500_000_000 : 1_500_000_000 // 0.5s vs 1.5s
                            
                             if isWiFi {
                                 self.log.info("On WiFi: Fast-tracking handshake (0.5s delay)")
                             } else {
                                 self.log.info("On AWDL: Slow-tracking handshake (1.5s delay) for stability")
                             }

                             try await Task.sleep(nanoseconds: delay)
                        
                        DispatchQueue.main.async { [weak self] in
                            guard let self = self else { return }
                            
                            // VALIDATION CHECKS
                            guard !self.isRestarting else { return }
                            guard session == self.session else { return }
                            guard session.connectedPeers.contains(peerID) else { return }
                            
                            // Send handshake when a peer connects (safely after delay)
                            self.sendHandshake()
                        }
                    } catch {
                        self.log.error("Traffic gating interrupted: \(error.localizedDescription)")
                    }
                }
                
            case .notConnected:
                 // Cancel per-peer watchdog timer
                 self.connectingTimeoutTimers[peerID]?.cancel()
                 self.connectingTimeoutTimers.removeValue(forKey: peerID)
                 
                 // Clean up NodeID tracking
                 if let nodeID = nodeID {
                     // Clear pending invite status
                     self.pendingInvites.removeValue(forKey: nodeID)
                 }
                
                // STABILITY CHECK: If we lose connection before Stability Timer fires, we KEEP the failure count.
                if let nodeID = nodeID, let timer = self.stabilityTimers[nodeID] {
                    self.log.warning("Connection to \(nodeID) dropped before stability period! Preserving failure count: \(self.consecutiveFailures[nodeID] ?? 0)")
                    timer.invalidate()
                    self.stabilityTimers.removeValue(forKey: nodeID)
                }

                // Check if we were previously connected to this peer
                let wasConnected = self.connectedPeers.contains(peerID)
                
                // PRESERVE PARTIAL TRANSCRIPT: If peer had a live transcript, post it as final message
                if let peerUser = self.connectedPeerUsers.first(where: { $0.id == peerID }) {
                    // liveTranscripts is now keyed by nodeID
                    let transcriptKey = peerUser.nodeID.isEmpty ? peerUser.name : peerUser.nodeID
                    if let partialText = self.liveTranscripts[transcriptKey], !partialText.isEmpty {
                        let finalMessage = MeshMessage(
                            id: UUID(),
                            senderID: peerUser.name,
                            senderNodeID: peerUser.nodeID,
                            senderRole: peerUser.role,
                            senderColorHex: peerUser.colorHex,
                            content: partialText,
                            isTranscription: true,
                            timestamp: Date()
                        )
                        self.receivedMessages.append(finalMessage)
                        self.log.info("Posted partial transcript from disconnected peer \(peerUser.name)")
                    }
                    self.liveTranscripts.removeValue(forKey: transcriptKey)
                }
                
                // Update connected peers list
                if let currentSession = self.session {
                    self.connectedPeers = currentSession.connectedPeers
                } else {
                    self.connectedPeers.removeAll { $0 == peerID }
                }
                
                // Remove from connectedPeerUsers
                self.connectedPeerUsers.removeAll { $0.id == peerID }
                
                // KeepAlive: Stop when transitioning from 1 → 0 peers
                if self.connectedPeers.isEmpty {
                    self.stopKeepAlive()
                }
                
                if wasConnected {
                    self.log.info("Connection dropped: \(peerID). Mesh size: \(self.connectedPeers.count)")
                }
                
                // UNIFIED RECOVERY: Treats both handshake failures AND drops as candidates for Smart Retry.
                // We NO LONGER restart services just because the mesh is empty, to prevent "Connection refused" errors for others.
                
                guard let nodeID = nodeID else {
                    self.log.info("Link lost for unknown nodeID peer. Ignoring.")
                    return
                }
                    
                    // GHOST CHECK: If this is an older instance, ignore the failure
                    if let latest = self.latestByNodeID[nodeID], latest.peerID != peerID {
                        self.log.info("Ignoring failure of GHOST peer \(peerID). Current is \(latest.peerID).")
                        return
                    }
                    
                    // Track consecutive failures for poisoned state detection
                    let failures = (self.consecutiveFailures[nodeID] ?? 0) + 1
                    self.consecutiveFailures[nodeID] = failures
                    self.log.info("Consecutive failures for nodeID \(nodeID): \(failures)")
                    
                    // Add exponential backoff cooldown
                    let backoff = self.calculateBackoff(failures: failures)
                    self.cooldownUntil[nodeID] = Date().addingTimeInterval(backoff)
                    self.log.info("Peer \(nodeID) in cooldown for \(backoff)s")
                    
                    // Don't restart if we have active connections
                    if !self.connectedPeers.isEmpty {
                        self.log.info("Handshake failed to \(nodeID), but we have active connections. Skipping restart.")
                        return
                    }
                    
                    // POISONED STATE: If too many consecutive failures, do full reset
                    if failures >= Self.poisonedThreshold {
                        self.log.error("POISONED STATE: \(failures) consecutive failures for \(nodeID). Full reset.")
                        self.restartServices(forcePoisonedRecovery: true)
                        return
                    }
                    
                    // SMART RETRY: Instead of restarting everything (Nuclear Option), just try to reconnect to this peer.
                    // Only LEADER initiates the retry to prevent race conditions.
                    if self.myNodeID > nodeID {
                        self.log.info("Handshake failed. I am LEADER. Scheduling Smart Retry in \(backoff)s.")
                        
                        DispatchQueue.main.asyncAfter(deadline: .now() + backoff) { [weak self] in
                            guard let self = self else { return }
                            
                            // Re-check conditions before firing
                            guard !self.connectedPeers.contains(peerID) else { return }
                            guard self.pendingInvites[nodeID] == nil else { return }
                            
                            // Ensure we still have a browser and session
                            guard let browser = self.serviceBrowser, let session = self.session else { return }
                            
                            self.log.info("Smart Retry: Re-inviting \(peerID) (nodeID: \(nodeID)).")
                            self.pendingInvites[nodeID] = peerID
                            
                            let contextData = try? JSONEncoder().encode([
                                "nodeID": self.myNodeID,
                                "instance": String(self.myInstance)
                            ])
                            
                            browser.invitePeer(peerID, to: session, withContext: contextData, timeout: 7)
                        }
                    } else {
                         self.log.info("Handshake failed. I am FOLLOWER. Waiting for Leader to retry.")
                    }

                
            case .connecting:
                // Per-peer watchdog: cancel only this peer on timeout, don't restart everything
                // Try reverse lookup if nodeID not in peerNodeIDMap yet (e.g. from Pending Invites)
                let resolvedNodeID = nodeID ?? self.pendingInvites.first(where: { $0.value == peerID })?.key
                
                guard let nodeID = resolvedNodeID else {
                    // This happens if THEY invited US and we haven't processed the context fully yet.
                    // Ideally, `advertiser` delegate should have populated peerNodeIDMap.
                    self.log.info("Connecting to peer with unknown nodeID: \(peerID) - Watchdog might be skipped if not resolved.")
                    return
                }
                
                // Cancel any existing timer for this specific socket
                self.connectingTimeoutTimers[peerID]?.cancel()
                
                // Capture expected instance to detect if peer updated while connecting
                let expectedInstance = self.latestByNodeID[nodeID]?.instance ?? 0
                
                // Longer timeout when not on WiFi (cellular AWDL is slower/unreliable)
                // OPTIMIZATION: Relax WiFi timeout to 6.0s (was 3.5s) to avoid premature timeouts
                let timeout: Double = NetworkMonitor.shared.isWiFi ? 6.0 : 15.0
                let item = DispatchWorkItem { [weak self] in
                    guard let self = self else { return }
                    
                    // ABORT GUARD: If we have since discovered a newer instance, don't punish this nodeID
                    if let current = self.latestByNodeID[nodeID], current.instance > expectedInstance {
                        self.log.info("Watchdog: Aborting timeout for peer \(peerID) (node \(nodeID)) - found newer instance \(current.instance) > \(expectedInstance)")
                        return
                    }
                    
                    self.log.error("Watchdog: Handshake TIMED OUT (\(timeout)s) for peer \(peerID).")
                    
                    // Kill THIS specific socket
                    self.session?.cancelConnectPeer(peerID)
                    
                    // Cancel pending invite logic ONLY if it matches this peer
                    if let peerToCancel = self.pendingInvites[nodeID], peerToCancel == peerID {
                        self.log.info("Watchdog: Clearing pending invite for \(nodeID)")
                        self.pendingInvites.removeValue(forKey: nodeID)
                    }
                    
                    // Track failure
                    let failures = (self.consecutiveFailures[nodeID] ?? 0) + 1
                    self.consecutiveFailures[nodeID] = failures
                    
                    // Add cooldown with exponential backoff
                    let backoff = self.calculateBackoff(failures: failures)
                    self.cooldownUntil[nodeID] = Date().addingTimeInterval(backoff)
                    self.log.info("Peer \(nodeID) quarantined for \(backoff)s (failure #\(failures))")
                    
                    // POISONED STATE: Full reset only if mesh is empty
                    if failures >= Self.poisonedThreshold {
                        if self.connectedPeers.isEmpty {
                            self.log.error("POISONED STATE: \(failures) consecutive failures. Full reset.")
                            self.restartServices(forcePoisonedRecovery: true)
                        } else {
                            self.log.error("Poisoned threshold hit for \(nodeID) but mesh is active; quarantining only.")
                        }
                        return
                    }
                    
                    // Only restart if mesh is empty (Legacy logic kept for safety but largely disabled by checks)
                    if self.connectedPeers.isEmpty {
                        self.scheduleRestartIfStillEmpty()
                    }
                }
                self.connectingTimeoutTimers[peerID] = item
                DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: item)
                
            @unknown default:
                break
            }
        }
    }
    
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        // Safe check on main thread? No, didReceive is high volume. 
        // We can just capture self.session in the async block or check it against the passed session.
        // But for data, it's less critical to check identity for *stablity* (only valid sessions receive data).
        // However, consistency is good.
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            guard session == self.session else { return }
            
            do {
                let message = try JSONDecoder().decode(MeshMessage.self, from: data)
                
                // GHOST FILTER: Ignore messages from older instances if we know a newer one
                if !message.senderNodeID.isEmpty {
                    if let latest = self.latestByNodeID[message.senderNodeID] {
                        if message.senderInstance > latest.instance {
                            // SELF-HEALING: We found a newer instance via Handshake that Browser didn't report yet.
                            // TRUST IT immediately to fix race conditions.
                            self.log.info("Ghost Filter: Updated latest instance for \(message.senderNodeID) to \(message.senderInstance) based on Handshake.")
                            self.latestByNodeID[message.senderNodeID] = (peerID, message.senderInstance)
                        } else if latest.peerID != peerID {
                            // It's genuinely an older instance (Ghost)
                             self.log.info("Ignoring message from GHOST peer \(peerID) (NodeID: \(message.senderNodeID)). Current is \(latest.peerID)")
                             return
                        }
                    }
                }
                
                // Keep-Alive / Gossip Handling
                if message.isKeepAlive || message.content == "💓" {
                    // Gossip: Check for missing peers
                    if let theirConnectedNodes = message.connectedNodeIDs {
                        // 1. Identify nodes they have that WE don't (clique gaps)
                        // Filter out empty IDs and our own ID
                        let missingNodeIDs = theirConnectedNodes.filter { nid in
                            !nid.isEmpty &&
                            nid != self.myNodeID &&
                            !self.connectedPeerUsers.contains(where: { $0.nodeID == nid })
                        }
                        
                        if !missingNodeIDs.isEmpty {
                            self.log.info("Gossip: Peer \(message.senderID) has connections I lack: \(missingNodeIDs)")
                            
                            for missingID in missingNodeIDs {
                                // 2. Check if we have discovered this missing node
                                if let discovery = self.latestByNodeID[missingID] {
                                    
                                    // 3. SAFETY CHECK: Are we already connected at the socket level?
                                    // If yes, DO NOT Invite. Just wait for the handshake.
                                    // (Gossip sees them as missing because they aren't in connectedPeerUSERS yet)
                                    if self.session?.connectedPeers.contains(discovery.peerID) == true {
                                        self.log.info("Gossip Repair: Ignoring missing \(missingID). Socket is connected to \(discovery.peerID), processing handshake...")
                                        continue
                                    }
                                    
                                    // We know them! REPAIR strategy:
                                    // If we are "Leader" relative to them, active invite.
                                    // If we are "Follower", we just log (or potentially bump state).
                                    // Note: Using string comparison for nodeID is consistent with Smart Retry.
                                    if self.myNodeID > missingID {
                                        self.log.info("Gossip Repair: I am LEADER for missing \(missingID) (discovered as \(discovery.peerID)). Triggering Invite.")
                                        // Re-use logic from foundPeer? Or just direct invite?
                                        // Direct invite safety check:
                                        if self.pendingInvites[missingID] == nil {
                                            self.log.info("Gossip Repair: Sending Invite to \(discovery.peerID)")
                                            // 2.0s delay to be safe, or immediate? 
                                            // Immediate is fine here as we've likely known them for a while if they are in 'latestByNodeID'.
                                            let contextData = self.createInviteContext()
                                            self.serviceBrowser?.invitePeer(discovery.peerID, to: self.session!, withContext: contextData, timeout: 10)
                                            self.pendingInvites[missingID] = discovery.peerID
                                        }
                                    } else {
                                        // REVERSE INVITE: Break the stalemate if Leader thinks we're connected but we aren't.
                                        if self.pendingInvites[missingID] == nil {
                                            self.log.info("Gossip Repair: I am FOLLOWER for missing \(missingID). Leader hasn't invited me (Half-Open?). TRIGGERING REVERSE INVITE.")
                                            self.log.info("Gossip Repair: Sending Reverse Invite to \(discovery.peerID)")
                                            let contextData = self.createInviteContext()
                                            self.serviceBrowser?.invitePeer(discovery.peerID, to: self.session!, withContext: contextData, timeout: 10)
                                            self.pendingInvites[missingID] = discovery.peerID
                                        }
                                    }
                                } else {
                                    // We haven't even discovered them yet. Nothing we can do but wait for Bonjour.
                                    // self.log.debug("Gossip: Missing \(missingID) is unknown to me (not in latestByNodeID).")
                                }
                            }
                        }
                    }
                    return
                }
                
                if message.isHandshake {
                    // Use nodeID for reliable identity mapping (required, not optional)
                    let nodeID = message.senderNodeID
                    
                    // CRITICAL FIX: Cancel watchdog timer now that we have a handshake
                // Cancel watchdog: Valid handshake received for THIS peer
                self.connectingTimeoutTimers[peerID]?.cancel()
                self.connectingTimeoutTimers.removeValue(forKey: peerID)
                self.log.info("Watchdog: Cancelled timer for peer \(peerID) (Handshake received)")
                    
                    // Update peerNodeIDMap with handshake info
                    if !nodeID.isEmpty {
                        self.peerNodeIDMap[peerID] = nodeID
                    }
                    
                    let newUser = PeerUser(
                        peerID: peerID,
                        nodeID: nodeID,
                        name: message.senderID,
                        colorHex: message.senderColorHex,
                        role: message.senderRole
                    )
                    
                    // DEATH SPIRAL FIX: Start Stability Timer (15s)
                    // We do NOT reset failures immediately. We wait to see if connection holds.
                    if !nodeID.isEmpty {
                        self.stabilityTimers[nodeID]?.invalidate() // Reset if exists
                        self.log.info("Starting 15s Stability Timer for \(nodeID)...")
                        
                        let timer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
                            guard let self = self else { return }
                            self.log.info("Stability Period Passed! Resetting failures for \(nodeID).")
                            self.consecutiveFailures[nodeID] = 0
                            self.cooldownUntil.removeValue(forKey: nodeID)
                            self.stabilityTimers.removeValue(forKey: nodeID)
                        }
                        self.stabilityTimers[nodeID] = timer
                    }
                    
                    // Match by nodeID (reliable) instead of displayName
                    if !nodeID.isEmpty, let index = self.connectedPeerUsers.firstIndex(where: { $0.nodeID == nodeID }) {
                        self.log.info("UI Update: Updating existing user for nodeID \(nodeID) (peerID mismatch allowed)")
                        self.connectedPeerUsers[index] = newUser
                    } else if let index = self.connectedPeerUsers.firstIndex(where: { $0.id == peerID }) {
                        // Fallback: match by MCPeerID
                        self.log.info("UI Update: Updating existing user for peerID \(peerID)")
                        self.connectedPeerUsers[index] = newUser
                    } else {
                        self.log.info("UI Update: Appending new user \(peerID) (nodeID: \(nodeID))")
                        self.connectedPeerUsers.append(newUser)
                    }
                    
                    self.log.info("Handshake received from \(message.senderID) (nodeID: \(nodeID))")
                } else if message.isPartial {
                    // Key by nodeID (we enforce senderNodeID on send, so this should always be set)
                    guard !message.senderNodeID.isEmpty else {
                        self.log.warning("Received partial with empty senderNodeID - ignoring")
                        return
                    }
                    if message.content.isEmpty {
                        self.liveTranscripts.removeValue(forKey: message.senderNodeID)
                    } else {
                        self.liveTranscripts[message.senderNodeID] = message.content
                    }
                } else {
                    // Final message received
                    self.receivedMessages.append(message)
                    // Clear the live transcript for this sender
                    if !message.senderNodeID.isEmpty {
                        self.liveTranscripts.removeValue(forKey: message.senderNodeID)
                    }
                }
            } catch {
                self.log.error("Error decoding message: \(error.localizedDescription)")
            }
        }
    }
    
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        log.error("Receiving streams is not supported")
    }
    
    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        log.error("Receiving resources is not supported")
    }
    
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        log.error("Receiving resources is not supported")
    }
}
