package com.parkplants.ui.addplant import android.content.Context import android.graphics.BitmapFactory import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkManager import com.parkplants.data.api.models.Nursery import com.parkplants.data.api.models.PlantType import com.parkplants.data.db.entities.PendingPlant import com.parkplants.data.repository.PlantRepository import com.parkplants.data.sync.SyncWorker import com.parkplants.ui.main.OverlayState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject data class AddPlantUiState( val plantTypes: List = emptyList(), val plantTypeSuggestions: List = emptyList(), val nurserySuggestions: List = emptyList(), val isLoadingTypes: Boolean = false, val isSaving: Boolean = false, val saved: Boolean = false, val error: String? = null, val createdType: PlantType? = null, ) @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @HiltViewModel class AddPlantViewModel @Inject constructor( private val plantRepository: PlantRepository, @ApplicationContext private val context: Context, ) : ViewModel() { private val _uiState = MutableStateFlow(AddPlantUiState()) val uiState = _uiState.asStateFlow() private val _overlayState = MutableStateFlow(null) val overlayState = _overlayState.asStateFlow() // Поиск с дебаунсом — не создаём корутину на каждый символ private val _typeSearchQuery = MutableStateFlow("") private val _nurserySearchQuery = MutableStateFlow("") init { loadPlantTypes() observeTypeSearch() observeNurserySearch() loadOverlay() } private fun loadOverlay() { viewModelScope.launch(Dispatchers.IO) { try { val config = plantRepository.loadOverlay() ?: return@launch val file = File(config.url) if (!file.exists()) return@launch val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeFile(config.url, bounds) var sampleSize = 1; var w = bounds.outWidth; var h = bounds.outHeight while (w > 8192 || h > 8192) { sampleSize *= 2; w /= 2; h /= 2 } val bitmap = BitmapFactory.decodeFile( config.url, BitmapFactory.Options().apply { inSampleSize = sampleSize inPreferredConfig = android.graphics.Bitmap.Config.ARGB_8888 inScaled = false } ) ?: return@launch _overlayState.value = OverlayState(config, bitmap) } catch (_: Exception) {} } } fun loadPlantTypes() { viewModelScope.launch { // 1. Сразу показываем кеш val cached = plantRepository.getCachedPlantTypes() if (cached.isNotEmpty()) { _uiState.value = _uiState.value.copy(plantTypes = cached) } _uiState.value = _uiState.value.copy(isLoadingTypes = true) // 2. Обновляем типы из сети plantRepository.refreshPlantTypesFromNetwork() val updated = plantRepository.getCachedPlantTypes() _uiState.value = _uiState.value.copy( plantTypes = updated.ifEmpty { cached }, isLoadingTypes = false, ) } // 3. Питомники — отдельная корутина, не блокирует загрузку типов viewModelScope.launch { plantRepository.syncNurseries() } } private fun observeTypeSearch() { viewModelScope.launch { _typeSearchQuery .debounce(250) .distinctUntilChanged() .flatMapLatest { query -> flow { val results = if (query.isBlank()) emptyList() else plantRepository.searchPlantTypes(query) emit(results) } } .collect { results -> _uiState.value = _uiState.value.copy(plantTypeSuggestions = results) } } } private fun observeNurserySearch() { viewModelScope.launch { _nurserySearchQuery .debounce(250) .distinctUntilChanged() .flatMapLatest { query -> flow { val results = if (query.isBlank()) emptyList() else plantRepository.searchNurseries(query) emit(results) } } .collect { results -> _uiState.value = _uiState.value.copy(nurserySuggestions = results) } } } fun searchPlantTypes(prefix: String) { _typeSearchQuery.value = prefix } fun clearPlantTypeSuggestions() { _typeSearchQuery.value = "" _uiState.value = _uiState.value.copy(plantTypeSuggestions = emptyList()) } fun searchNurseries(prefix: String) { _nurserySearchQuery.value = prefix } fun clearNurserySuggestions() { _nurserySearchQuery.value = "" _uiState.value = _uiState.value.copy(nurserySuggestions = emptyList()) } fun createPlantType(name: String) { viewModelScope.launch { plantRepository.createPlantType(name, null).onSuccess { type -> _uiState.value = _uiState.value.copy(createdType = type) loadPlantTypes() } } } fun clearCreatedType() { _uiState.value = _uiState.value.copy(createdType = null) } fun savePlant( plantTypeId: Int, plantTypeName: String, lat: Double, lng: Double, plantedAt: String?, nursery: String?, photoPath: String?, ) { viewModelScope.launch { _uiState.value = _uiState.value.copy(isSaving = true, error = null) try { val pendingPlant = PendingPlant( plantTypeId = plantTypeId, plantTypeName = plantTypeName, lat = lat, lng = lng, plantedAt = plantedAt, nursery = nursery, localPhotoPath = photoPath, photoUrl = null, ) plantRepository.savePlantLocally(pendingPlant) // Сохранили локально — сразу показываем успех, не ждём сеть _uiState.value = _uiState.value.copy(isSaving = false, saved = true) // Фоновая попытка сразу + WorkManager для надёжности viewModelScope.launch { try { plantRepository.syncAll() } catch (_: Exception) { } } WorkManager.getInstance(context).enqueue(SyncWorker.buildRequest()) } catch (e: Exception) { _uiState.value = _uiState.value.copy( isSaving = false, error = e.message ?: "Ошибка сохранения", ) } } } fun resetSaved() { _uiState.value = _uiState.value.copy(saved = false) } }