import Foundation import Combine /// Root application state. Owns the long-lived services and wires them together. /// /// The central philosophy: **Presence is foreground-first.** The app is not a /// passive background listener — it is an active instrument you hold while you /// are out in the world. This is a feature, not a limitation. @MainActor final class AppState: ObservableObject { // MARK: - Published state /// Whether the user is currently "open to connection" and the app is /// actively broadcasting + listening. This is the master switch. @Published var isPresent: Bool = false /// The user's chosen radius tier. Defaults to "right here" (UWB). @Published var radiusTier: DistanceTier = .rightHere /// The current proximity engine (UWB / BLE / beacon) in use. @Published private(set) var activeEngine: ProximityEngine = .uwb // MARK: - Debug / simulation state #if DEBUG /// Whether we're using the simulated adapter (no hardware). @Published var isSimulated = true /// When true, the simulated adapter emits nearby users on a timer. @Published var autoBroadcast = false { didSet { configureSimulation() } } let simulatedAdapter = SimulatedAdapter() #endif // MARK: - Services let presenceEngine: PresenceEngine let tokenManager: TokenManager let cryptoManager: CryptoManager let apiClient: APIClient let authService: AuthService let profileImportService: ProfileImportService let graphStore: GraphStore let realtime: RealtimeClient let revealFlow: RevealFlow let chatViewModel: ChatViewModel let presenceViewModel: PresenceViewModel private var cancellables = Set() init() { let tokenManager = TokenManager() let cryptoManager = CryptoManager() #if DEBUG // In the simulator, point at the local reference server. let apiClient = APIClient.local() #else let apiClient = APIClient() #endif self.tokenManager = tokenManager self.cryptoManager = cryptoManager self.apiClient = apiClient let authService = AuthService(apiClient: apiClient) self.authService = authService let profileImportService = ProfileImportService() self.profileImportService = profileImportService let graphStore = GraphStore() self.graphStore = graphStore let realtime = RealtimeClient() self.realtime = realtime let revealFlow = RevealFlow( apiClient: apiClient, cryptoManager: cryptoManager, profileService: profileImportService, graphStore: graphStore, realtime: realtime ) self.revealFlow = revealFlow let chatViewModel = ChatViewModel( apiClient: apiClient, cryptoManager: cryptoManager, graphStore: graphStore, realtime: realtime ) self.chatViewModel = chatViewModel let presenceEngine: PresenceEngine #if DEBUG // In DEBUG (simulator), drive the engine with the simulated adapter // so the whole interaction loop can be tested without hardware. presenceEngine = PresenceEngine( tokenManager: tokenManager, cryptoManager: cryptoManager, apiClient: apiClient, uwbAdapter: simulatedAdapter, bleAdapter: simulatedAdapter, beaconAdapter: simulatedAdapter ) #else presenceEngine = PresenceEngine( tokenManager: tokenManager, cryptoManager: cryptoManager, apiClient: apiClient ) #endif self.presenceEngine = presenceEngine self.presenceViewModel = PresenceViewModel(presenceEngine: presenceEngine) // When the user toggles presence, drive the engine and open the // live socket so mutual waves and messages arrive in real time. $isPresent .sink { [weak presenceEngine, weak realtime] present in Task { await presenceEngine?.setPresent(present) if present { realtime?.connect(token: tokenManager.currentToken()) } else { realtime?.disconnect() } } } .store(in: &cancellables) $radiusTier .sink { [weak presenceEngine] tier in Task { await presenceEngine?.setRadiusTier(tier) } } .store(in: &cancellables) #if DEBUG configureSimulation() #endif } // MARK: - Debug / simulation #if DEBUG /// Forward the simulated adapter's events into the engine and set up /// auto-broadcast when enabled. private func configureSimulation() { simulatedAdapter.autoBroadcast = autoBroadcast // The engine already wired onEncounter/onBroadcast/onRegion to the // simulated adapter at init. Auto-broadcast is handled internally. } /// Used by the debug menu to simulate a single nearby user. func simulateEncounter() { simulatedAdapter.simulateEncounter(remoteToken: "sim-\(Int.random(in: 100_000...999_999))") } /// Used by the debug menu to simulate a crowd. func simulateCrowd() { simulatedAdapter.simulateCrowd(count: 20) } /// Reconnect the realtime socket (e.g. after starting the local server). func restartRealtime() { realtime.connect(token: tokenManager.currentToken()) } #endif }