package com.tt.honeyDue.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tt.honeyDue.models.NotificationPreference import com.tt.honeyDue.models.UpdateNotificationPreferencesRequest import com.tt.honeyDue.network.ApiResult import com.tt.honeyDue.network.APILayer import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch /** * Stable channel-id list used by the per-category notification UI. Keep * in lockstep with the Android `NotificationChannels` object (the same * four ids) — the parity is enforced by * `NotificationPreferencesScreenTest.categoryKeys_matchNotificationChannels`. */ object NotificationCategoryKeys { const val TASK_REMINDER = "task_reminder" const val TASK_OVERDUE = "task_overdue" const val RESIDENCE_INVITE = "residence_invite" const val SUBSCRIPTION = "subscription" val ALL: List = listOf( TASK_REMINDER, TASK_OVERDUE, RESIDENCE_INVITE, SUBSCRIPTION, ) } /** * Platform-agnostic façade around the per-category notification preference * store. The Android implementation wires `loadAll` / `setCategory` / * `setAll` to `NotificationPreferencesStore` (DataStore-backed); tests in * `commonTest` wire them to an in-memory fake. * * Kept in the viewmodel package (not `ui.screens`) so it can be referenced * from `commonTest` without pulling in Compose types. */ class NotificationCategoriesController( private val loadAll: suspend () -> Map, private val setCategory: suspend (String, Boolean) -> Unit, private val setAll: suspend (Boolean) -> Unit, ) { suspend fun load(): Map = loadAll() suspend fun onCategoryToggle(channelId: String, enabled: Boolean) { setCategory(channelId, enabled) } suspend fun onMasterToggle(enabled: Boolean) { setAll(enabled) } companion object { /** * Compute the master toggle's visible state from the current * per-category snapshot. Master is "on" iff every category is on * AND the snapshot is non-empty (an empty snapshot is the * "no data yet" state, which the UI renders as master-off). */ fun computeMasterState(snapshot: Map): Boolean = snapshot.isNotEmpty() && snapshot.values.all { it } } } /** * Existing remote-preferences ViewModel (server-backed). Retained * unchanged so the legacy Daily Digest / email prefs continue to work * alongside the new per-category local toggles driven by * [NotificationCategoriesController]. */ class NotificationPreferencesViewModel : ViewModel() { private val _preferencesState = MutableStateFlow>(ApiResult.Idle) val preferencesState: StateFlow> = _preferencesState.asStateFlow() private val _updateState = MutableStateFlow>(ApiResult.Idle) val updateState: StateFlow> = _updateState.asStateFlow() /** * Per-category local toggle state, keyed by [NotificationCategoryKeys] * channel ids. Backed on Android by `NotificationPreferencesStore` via * [NotificationCategoriesController]. */ private val _categoryState = MutableStateFlow>( NotificationCategoryKeys.ALL.associateWith { true }, ) val categoryState: StateFlow> = _categoryState.asStateFlow() fun loadPreferences() { viewModelScope.launch { _preferencesState.value = ApiResult.Loading val result = APILayer.getNotificationPreferences() _preferencesState.value = when (result) { is ApiResult.Success -> ApiResult.Success(result.data) is ApiResult.Error -> result else -> ApiResult.Error("Unknown error") } } } fun updatePreference( taskDueSoon: Boolean? = null, taskOverdue: Boolean? = null, taskCompleted: Boolean? = null, taskAssigned: Boolean? = null, residenceShared: Boolean? = null, warrantyExpiring: Boolean? = null, dailyDigest: Boolean? = null, emailTaskCompleted: Boolean? = null, taskDueSoonHour: Int? = null, taskOverdueHour: Int? = null, warrantyExpiringHour: Int? = null, dailyDigestHour: Int? = null, ) { viewModelScope.launch { _updateState.value = ApiResult.Loading val request = UpdateNotificationPreferencesRequest( taskDueSoon = taskDueSoon, taskOverdue = taskOverdue, taskCompleted = taskCompleted, taskAssigned = taskAssigned, residenceShared = residenceShared, warrantyExpiring = warrantyExpiring, dailyDigest = dailyDigest, emailTaskCompleted = emailTaskCompleted, taskDueSoonHour = taskDueSoonHour, taskOverdueHour = taskOverdueHour, warrantyExpiringHour = warrantyExpiringHour, dailyDigestHour = dailyDigestHour, ) val result = APILayer.updateNotificationPreferences(request) _updateState.value = when (result) { is ApiResult.Success -> { _preferencesState.value = ApiResult.Success(result.data) ApiResult.Success(result.data) } is ApiResult.Error -> result else -> ApiResult.Error("Unknown error") } } } fun resetUpdateState() { _updateState.value = ApiResult.Idle } // --------------------------------------------------------------------- // Per-category (local) toggle state — wired to platform-specific store. // --------------------------------------------------------------------- /** * Attach a [NotificationCategoriesController] and do an initial load * so the screen can render in sync with the on-disk preferences. Safe * to call multiple times; later calls replace the controller. */ fun attachCategoriesController(controller: NotificationCategoriesController) { this.categoriesController = controller viewModelScope.launch { _categoryState.value = controller.load() } } fun toggleCategory(channelId: String, enabled: Boolean) { val controller = categoriesController ?: return // Optimistic local update so the Switch flips immediately. _categoryState.value = _categoryState.value.toMutableMap().apply { put(channelId, enabled) } viewModelScope.launch { controller.onCategoryToggle(channelId, enabled) } } fun toggleMaster(enabled: Boolean) { val controller = categoriesController ?: return _categoryState.value = NotificationCategoryKeys.ALL.associateWith { enabled } viewModelScope.launch { controller.onMasterToggle(enabled) } } private var categoriesController: NotificationCategoriesController? = null }