package com.suno.android.ui.screens.home.profile.edit import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.suno.android.common_core_utils.global_errors.DialogSpec import com.suno.android.common_core_utils.global_errors.TopLevelErrorManager import com.suno.android.common_core_utils.model.UiString import com.suno.android.common_core_utils.model.Url import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.entities.ImageUploadConfig import com.suno.android.common_data.media.usecase.CleanupUriUseCase import com.suno.android.common_data.media.usecase.CreatePhotoUriUseCase import com.suno.android.common_data.use_case.ImageValidationError import com.suno.android.common_data.use_case.UriToBase64DataUseCase import com.suno.android.common_data.use_case.ValidateImageUploadUseCase import com.suno.android.common_data.user.UserSessionRepository import com.suno.android.common_mvi.MviProcessorFactory import com.suno.android.common_mvi.MviViewModel import com.suno.android.common_networking.extensions.toThrowable import com.suno.android.common_networking.remote.entities.UpdateArtistProfileSpec import com.suno.android.common_networking.remote.profiles.ProfilesService import com.suno.android.common_res.R import com.suno.android.gating.statsig.FeatureGate import com.suno.android.gating.statsig.StatsigFeatureDataSource import dagger.Lazy import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class EditProfileScreenVM @Inject constructor( processorFactory: MviProcessorFactory, private val userSessionRepository: UserSessionRepository, private val profilesService: ProfilesService, statsigManager: StatsigFeatureDataSource, private val topLevelErrorManager: TopLevelErrorManager, private val uriToBase64DataUseCase: UriToBase64DataUseCase, private val validateImageUploadUseCase: ValidateImageUploadUseCase, private val createPhotoUriUseCase: Lazy, private val cleanupPhotoUriUseCase: Lazy, ) : MviViewModel< EditProfileScreenEvent, EditProfileScreenState, EditProfileScreenEffect, >( processorFactory = processorFactory, initialState = EditProfileScreenState( user = userSessionRepository.sessionConfigurationStateFlow().value.user, isAddProfilePictureGateEnabled = statsigManager.checkGate(FeatureGate.ADD_PROFILE_PICTURE), ), ) { init { userSessionRepository.sessionConfigurationStateFlow() .onEach { sessionConfiguration -> sessionConfiguration.user?.let { user -> sendEvent(EditProfileScreenEvent.Internal.UpdateUser(user = user)) } }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) } override suspend fun reduceEvent( currentState: EditProfileScreenState, event: EditProfileScreenEvent, emitEffect: suspend (EditProfileScreenEffect) -> Unit, ): EditProfileScreenState { return when (event) { is EditProfileScreenEvent.Internal.UpdateUser -> { currentState.copy( user = event.user, userHandle = event.user.handle?.let(::UserHandle), displayName = event.user.displayName.orEmpty(), avatarImageUrl = event.user.avatarImageUrl?.let(::Url), ) } is EditProfileScreenEvent.Internal.UpdateIsPhotoUploading -> { currentState.copy( isPhotoUploading = event.isUploading, ) } is EditProfileScreenEvent.OnChangeUserHandle -> { updateUserProfile( displayName = currentState.displayName, handle = event.userHandle.handle, avatarImageUrl = currentState.avatarImageUrl?.url, ).fold( onSuccess = {}, onFailure = { error -> showUndescriptiveError() }, ) currentState.copy(userHandle = event.userHandle) } is EditProfileScreenEvent.OnChangeDisplayName -> { val handle = currentState.userHandle?.handle ?: return currentState updateUserProfile( displayName = event.displayName, handle = handle, avatarImageUrl = currentState.avatarImageUrl?.url, ).fold( onSuccess = {}, onFailure = { error -> showUndescriptiveError() }, ) currentState.copy(displayName = event.displayName) } is EditProfileScreenEvent.OnCameraRequested -> { val uri = createPhotoUriUseCase.get().invoke("temp_profile_image") if (uri == null) { currentState } else { emitEffect(EditProfileScreenEffect.LaunchCamera(uri)) currentState.copy(photoUri = uri) } } is EditProfileScreenEvent.OnImagePickerRequested -> { emitEffect(EditProfileScreenEffect.LaunchImagePicker) currentState } is EditProfileScreenEvent.OnCameraPermissionResult -> { if (event.isGranted) { emitEffect(EditProfileScreenEffect.RequestTakePhoto) } else { topLevelErrorManager.broadcastTopLevelDialogError( errorMessage = DialogSpec( title = UiString.Resource(R.string.permission_required), body = UiString.Resource(R.string.camera_permission_required), buttons = persistentListOf( DialogSpec.Button( label = UiString.Resource(R.string.ok), type = DialogSpec.Button.DialogButtonType.PRIMARY, ), ), ), ) } currentState } is EditProfileScreenEvent.OnCameraResult -> { if (event.success && currentState.photoUri != null) { processAndUploadImage(uri = currentState.photoUri) ?: currentState } else { currentState } } is EditProfileScreenEvent.OnImagePickerResult -> { if (event.uri == null) { currentState } else { processAndUploadImage(uri = event.uri) ?: currentState } } } } private suspend fun processAndUploadImage( uri: android.net.Uri, ): EditProfileScreenState? { // Validate image first val validationResult = validateImageUploadUseCase(uri, ImageUploadConfig.PROFILE_PHOTO) val validationError = validationResult.leftOrNull() if (validationError != null) { logger.e(Exception("Image validation failed: $validationError")) showImageValidationError(validationError) return null } // Process and convert to base64 val base64Data = uriToBase64DataUseCase(uri, ImageUploadConfig.PROFILE_PHOTO) return if (base64Data != null) { uploadProfileImage(base64Data, uri) } else { showUndescriptiveError() null } } private suspend fun uploadProfileImage( base64Data: String, uri: android.net.Uri, ): EditProfileScreenState? { sendEvent(EditProfileScreenEvent.Internal.UpdateIsPhotoUploading(true)) val spec = UpdateArtistProfileSpec( displayName = state.value.user?.displayName.orEmpty(), handle = state.value.user?.handle.orEmpty(), profileDescription = state.value.user?.profileDescription.orEmpty(), avatarImageUrl = base64Data, ) val result = profilesService.updateArtistProfile(spec) sendEvent(EditProfileScreenEvent.Internal.UpdateIsPhotoUploading(false)) return result.fold( ifLeft = { error -> logger.e(error.toThrowable()) topLevelErrorManager.broadcastTopLevelDialogError( errorMessage = DialogSpec( title = UiString.Resource(R.string.error_title), body = UiString.Resource(R.string.upload_failed), buttons = persistentListOf( DialogSpec.Button( label = UiString.Resource(R.string.ok), type = DialogSpec.Button.DialogButtonType.PRIMARY, ), ), ), ) null }, ifRight = { userSessionRepository.refreshUserSessionConfiguration() state.value.copy(avatarImageUrl = Url(uri.toString())) }, ) } private suspend fun showImageValidationError( error: ImageValidationError, ) { val errorMessage = when (error) { is ImageValidationError.FileTooLarge -> UiString.Resource(R.string.image_file_too_large) is ImageValidationError.UnsupportedFormat -> UiString.Resource(R.string.unsupported_image_format) is ImageValidationError.FileNotFound, is ImageValidationError.UnknownError, -> UiString.Resource(R.string.image_loading_failed) } topLevelErrorManager.broadcastTopLevelDialogError( errorMessage = DialogSpec( title = UiString.Resource(R.string.error_title), body = errorMessage, buttons = persistentListOf( DialogSpec.Button( label = UiString.Resource(R.string.ok), type = DialogSpec.Button.DialogButtonType.PRIMARY, ), ), ), ) } private suspend fun updateUserProfile( displayName: String, handle: String, avatarImageUrl: String?, ): Result { val spec = UpdateArtistProfileSpec( displayName = displayName, handle = handle, profileDescription = displayName, avatarImageUrl = avatarImageUrl, ) profilesService.updateArtistProfile(spec).getOrElse { error -> logger.e(error.toThrowable()) return Result.failure(error.toThrowable()) } userSessionRepository.refreshUserSessionConfiguration() return Result.success(Unit) } private suspend fun showUndescriptiveError() { topLevelErrorManager.broadcastTopLevelDialogError( errorMessage = DialogSpec( title = UiString.Resource(R.string.error_title), body = UiString.Resource(R.string.action_failed), buttons = persistentListOf( DialogSpec.Button( label = UiString.Resource(R.string.ok), type = DialogSpec.Button.DialogButtonType.PRIMARY, ), ), ), ) } override fun onCleared() { super.onCleared() state.value.photoUri?.let { uri -> viewModelScope.launch { cleanupPhotoUriUseCase.get().invoke(uri) } } } }