package com.shaarit.presentation.todo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.shaarit.data.sync.SyncManager import com.shaarit.data.worker.TodoNotificationScheduler import com.shaarit.domain.model.SubTask import com.shaarit.domain.model.TodoItem import com.shaarit.domain.repository.GeminiRepository import com.shaarit.domain.repository.TodoRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.util.UUID import javax.inject.Inject data class EditableBrainDumpTask( val localId: String = UUID.randomUUID().toString(), val title: String, val dueDate: Long? = null, val tags: List = emptyList(), val groupName: String? = null ) data class BrainDumpDialogUiState( val input: String = "", val isAnalyzing: Boolean = false, val isSaving: Boolean = false, val parsedTasks: List = emptyList(), val errorMessage: String? = null, val saveCompleted: Boolean = false, val requestNotificationPermission: Boolean = false ) data class EditTodoDialogUiState( val isVisible: Boolean = false, val todoId: Long = 0, val content: String = "", val dueDate: Long? = null, val tags: List = emptyList(), val groupName: String = "", val subtasks: List = emptyList(), val newSubtaskText: String = "", val isSaving: Boolean = false, val errorMessage: String? = null, val shaarliLinkUrl: String = "", val isDone: Boolean = false ) @HiltViewModel class TodoViewModel @Inject constructor( private val todoRepository: TodoRepository, private val geminiRepository: GeminiRepository, private val syncManager: SyncManager, private val notificationScheduler: TodoNotificationScheduler ) : ViewModel() { val todos: StateFlow> = todoRepository .getTodosStream() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) val groupNames: StateFlow> = todoRepository .getGroupNamesStream() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) private val _selectedGroup = MutableStateFlow(null) val selectedGroup: StateFlow = _selectedGroup.asStateFlow() private val _dialogState = MutableStateFlow(BrainDumpDialogUiState()) val dialogState: StateFlow = _dialogState.asStateFlow() private val _editDialogState = MutableStateFlow(EditTodoDialogUiState()) val editDialogState: StateFlow = _editDialogState.asStateFlow() fun selectGroup(group: String?) { _selectedGroup.value = group } // ====== Brain Dump Dialog ====== fun onBrainDumpInputChanged(value: String) { _dialogState.update { it.copy(input = value, errorMessage = null) } } fun appendVoiceInput(transcript: String) { val cleaned = transcript.trim() if (cleaned.isBlank()) return _dialogState.update { val separator = if (it.input.isBlank()) "" else "\n" it.copy(input = it.input + separator + cleaned) } } fun analyzeBrainDump() { val input = _dialogState.value.input.trim() if (input.isBlank()) { _dialogState.update { it.copy(errorMessage = "Le brain dump ne peut pas être vide") } return } if (!geminiRepository.isApiKeyConfigured()) { _dialogState.update { it.copy(errorMessage = "Clé API Gemini non configurée dans les paramètres") } return } viewModelScope.launch { _dialogState.update { it.copy(isAnalyzing = true, errorMessage = null, parsedTasks = emptyList()) } runCatching { geminiRepository.analyzeBrainDump(input) }.onSuccess { result -> val parsedTasks = result.tasks.map { task -> EditableBrainDumpTask( title = task.title, dueDate = task.dueDate, tags = task.tags ) }.ifEmpty { listOf( EditableBrainDumpTask( title = input.take(80), dueDate = null, tags = emptyList() ) ) } _dialogState.update { it.copy( isAnalyzing = false, parsedTasks = parsedTasks, errorMessage = null ) } }.onFailure { error -> _dialogState.update { it.copy( isAnalyzing = false, errorMessage = error.message ?: "Erreur pendant l'analyse IA" ) } } } } fun updateTaskTitle(taskId: String, title: String) { _dialogState.update { state -> state.copy( parsedTasks = state.parsedTasks.map { task -> if (task.localId == taskId) task.copy(title = title) else task } ) } } fun updateTaskDueDate(taskId: String, dueDate: Long?) { _dialogState.update { state -> state.copy( parsedTasks = state.parsedTasks.map { task -> if (task.localId == taskId) task.copy(dueDate = dueDate) else task } ) } } fun updateTaskGroup(taskId: String, groupName: String?) { _dialogState.update { state -> state.copy( parsedTasks = state.parsedTasks.map { task -> if (task.localId == taskId) task.copy(groupName = groupName) else task } ) } } fun removeTag(taskId: String, tag: String) { _dialogState.update { state -> state.copy( parsedTasks = state.parsedTasks.map { task -> if (task.localId == taskId) { task.copy(tags = task.tags.filterNot { it.equals(tag, ignoreCase = true) }) } else { task } } ) } } fun saveParsedTasks() { val tasksToSave = _dialogState.value.parsedTasks .map { task -> task.copy( title = task.title.trim(), tags = task.tags .map { it.trim().trimStart('#').lowercase() } .filter { it.isNotBlank() } .distinct() ) } .filter { it.title.isNotBlank() } if (tasksToSave.isEmpty()) { _dialogState.update { it.copy(errorMessage = "Aucune tâche valide à sauvegarder") } return } viewModelScope.launch { _dialogState.update { it.copy(isSaving = true, errorMessage = null) } var hasError = false tasksToSave.forEach { task -> val result = todoRepository.upsertTodo( TodoItem( shaarliLinkUrl = "", content = task.title, isDone = false, dueDate = task.dueDate, tags = task.tags, isSynced = false, groupName = task.groupName?.takeIf { it.isNotBlank() } ) ) if (result.isFailure) { hasError = true } } syncManager.syncNow() val shouldAskPermission = tasksToSave.any { it.dueDate != null } && notificationScheduler.requiresNotificationPermission() _dialogState.update { it.copy( isSaving = false, saveCompleted = !hasError, errorMessage = if (hasError) "Certaines tâches n'ont pas pu être sauvegardées" else null, requestNotificationPermission = shouldAskPermission ) } } } fun consumeSaveCompleted() { _dialogState.update { it.copy( saveCompleted = false, input = "", parsedTasks = emptyList(), errorMessage = null ) } } fun clearDialogState() { _dialogState.update { BrainDumpDialogUiState() } } fun onNotificationPermissionHandled() { _dialogState.update { it.copy(requestNotificationPermission = false) } } // ====== Edit Todo Dialog ====== fun openEditDialog(todoId: Long) { viewModelScope.launch { val todo = todoRepository.getTodoById(todoId) ?: return@launch _editDialogState.value = EditTodoDialogUiState( isVisible = true, todoId = todo.id, content = todo.content, dueDate = todo.dueDate, tags = todo.tags, groupName = todo.groupName ?: "", subtasks = todo.subtasks, shaarliLinkUrl = todo.shaarliLinkUrl, isDone = todo.isDone ) } } fun closeEditDialog() { _editDialogState.value = EditTodoDialogUiState() } fun onEditContentChanged(value: String) { _editDialogState.update { it.copy(content = value, errorMessage = null) } } fun onEditDueDateChanged(dueDate: Long?) { _editDialogState.update { it.copy(dueDate = dueDate) } } fun onEditGroupChanged(groupName: String) { _editDialogState.update { it.copy(groupName = groupName) } } fun onEditNewSubtaskTextChanged(text: String) { _editDialogState.update { it.copy(newSubtaskText = text) } } fun addSubtask() { val text = _editDialogState.value.newSubtaskText.trim() if (text.isBlank()) return _editDialogState.update { it.copy( subtasks = it.subtasks + SubTask(content = text), newSubtaskText = "" ) } } fun removeSubtask(index: Int) { _editDialogState.update { it.copy(subtasks = it.subtasks.toMutableList().apply { removeAt(index) }) } } fun toggleSubtask(index: Int) { _editDialogState.update { val mutable = it.subtasks.toMutableList() val sub = mutable[index] mutable[index] = sub.copy(isDone = !sub.isDone) it.copy(subtasks = mutable) } } fun updateSubtaskContent(index: Int, content: String) { _editDialogState.update { val mutable = it.subtasks.toMutableList() mutable[index] = mutable[index].copy(content = content) it.copy(subtasks = mutable) } } fun saveEditedTodo() { val state = _editDialogState.value val content = state.content.trim() if (content.isBlank()) { _editDialogState.update { it.copy(errorMessage = "Le contenu ne peut pas être vide") } return } viewModelScope.launch { _editDialogState.update { it.copy(isSaving = true, errorMessage = null) } val result = todoRepository.upsertTodo( TodoItem( id = state.todoId, shaarliLinkUrl = state.shaarliLinkUrl, content = content, isDone = state.isDone, dueDate = state.dueDate, tags = state.tags, isSynced = false, groupName = state.groupName.takeIf { it.isNotBlank() }, subtasks = state.subtasks.filter { it.content.isNotBlank() } ) ) if (result.isSuccess) { syncManager.syncNow() _editDialogState.value = EditTodoDialogUiState() } else { _editDialogState.update { it.copy( isSaving = false, errorMessage = "Erreur lors de la sauvegarde" ) } } } } // ====== Actions ====== fun toggleTodo(todoId: Long, isDone: Boolean) { viewModelScope.launch { todoRepository.toggleDone(todoId, isDone) } } fun snoozeTodo(todoId: Long) { viewModelScope.launch { todoRepository.snoozeTodo(todoId) } } fun deleteTodo(todoId: Long) { viewModelScope.launch { todoRepository.deleteTodo(todoId) } } }