package com.suno.android.common_data.repos import arrow.core.Either import arrow.core.flatMap import arrow.core.getOrElse import arrow.retrofit.adapter.either.networkhandling.UnexpectedCallError import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_data.mappers.clips.LocalClipData import com.suno.android.common_data.mappers.clips.xAsLocalClipData import com.suno.android.common_data.mappers.hooks.HookDownloadResponse import com.suno.android.common_data.mappers.hooks.HooksFeed import com.suno.android.common_data.mappers.hooks.HooksShortcutCarousel import com.suno.android.common_data.mappers.hooks.LocalHookData import com.suno.android.common_data.mappers.hooks.Lyrics import com.suno.android.common_data.mappers.hooks.toHookDownloadResponse import com.suno.android.common_data.mappers.hooks.xAsHooksFeed import com.suno.android.common_data.mappers.hooks.xAsLocalHookData import com.suno.android.common_data.mappers.hooks.xAsTabCarousel import com.suno.android.common_data.mappers.lyrics.xAsLyrics import com.suno.android.common_data.metadata.RecommendationMetadata import com.suno.android.common_data.metadata.toRemoteMetadata import com.suno.android.common_networking.extensions.ApiResult import com.suno.android.common_networking.extensions.toThrowable import com.suno.android.common_networking.remote.entities.RemoteHookReactionBody import com.suno.android.common_networking.remote.entities.RemoteReportHookBody import com.suno.android.common_networking.remote.entities.hooks.RemoteFetchHooksLyricsBody import com.suno.android.common_networking.remote.entities.hooks.RemoteHooksFeedBody import com.suno.android.common_networking.remote.entities.hooks.RemoteShareHookBody import com.suno.android.common_networking.remote.entities.hooks.RemoteTabCarouselBody import com.suno.android.common_networking.remote.entities.hooks.watched.RemoteUpdatePlayCountsBody import com.suno.android.common_networking.remote.hooks.HooksService import javax.inject.Inject /** * Repository that fetches hooks feed data from the network and returns client models. */ interface HooksRepository { suspend fun getHooksFeed( startIndex: Int, ): ApiResult suspend fun setHookReaction( hookId: Id, action: RemoteHookReactionBody.Action, recommendationMetadata: RecommendationMetadata, tapType: RemoteHookReactionBody.TapType? = null, ): ApiResult suspend fun getHooksCreateSuggestedClips( searchTerm: String? = null, isLiked: Boolean? = null, isPublicOnly: Boolean? = null, pageSize: Int? = null, startIndex: Int? = null, ): ApiResult> suspend fun reportInappropriate( hookId: Id, recommendationMetadata: RecommendationMetadata, ): ApiResult suspend fun updatePlayCounts( playCounts: Map, Int>, ): ApiResult suspend fun getUserVideoHooksV2( startIndex: Int? = null, pageSize: Int? = null, userHandle: String? = null, ): ApiResult suspend fun getHooksCarouselContent(): Either suspend fun getSingleHookFeed( hookId: Id, ): ApiResult suspend fun incrementShareCount( hookId: Id, recommendationMetadata: RecommendationMetadata, ): ApiResult suspend fun getHooksLyrics( hooks: List>, ): Either, Lyrics>> suspend fun getHookDownloadUrl( hookId: Id, ): ApiResult } class DefaultHooksRepository @Inject constructor( loggerFactory: SunoLogger.Factory, private val hooksService: HooksService, ) : HooksRepository { private val logger = loggerFactory.create(this@DefaultHooksRepository) override suspend fun getHooksFeed( startIndex: Int, ): ApiResult { val body = RemoteHooksFeedBody( startIndex = startIndex, pageSize = PAGE_SIZE, ) return hooksService.getHooksFeed(body).map { response -> response.xAsHooksFeed() } } override suspend fun setHookReaction( hookId: Id, action: RemoteHookReactionBody.Action, recommendationMetadata: RecommendationMetadata, tapType: RemoteHookReactionBody.TapType?, ): ApiResult { val body = RemoteHookReactionBody( action = action, recommendationMetadata = recommendationMetadata.toRemoteMetadata(), tapType = tapType, ) return hooksService.setHookReaction(hookId.value, body) } override suspend fun getHooksCreateSuggestedClips( searchTerm: String?, isLiked: Boolean?, isPublicOnly: Boolean?, pageSize: Int?, startIndex: Int?, ): ApiResult> = hooksService.getHookCreateSuggestedClips( searchTerm = searchTerm, isLiked = isLiked, isPublicOnly = isPublicOnly, pageSize = pageSize, startIndex = startIndex, ).map { response -> response.clips.map { it.xAsLocalClipData() } } override suspend fun reportInappropriate( hookId: Id, recommendationMetadata: RecommendationMetadata, ): ApiResult { val body = RemoteReportHookBody(recommendationMetadata = recommendationMetadata.toRemoteMetadata()) return hooksService.reportHook(hookId.value, body) } override suspend fun updatePlayCounts( playCounts: Map, Int>, ): ApiResult { val hookIdsToTimesListened = playCounts.mapKeys { (hookId, _) -> hookId.value } val body = RemoteUpdatePlayCountsBody(hookIdsToTimesListened) return hooksService.updatePlayCounts(body).flatMap { response -> if (response.success) { Either.Right(Unit) } else { Either.Left(UnexpectedCallError(Throwable("Unable to update play counts"))) } } } override suspend fun getUserVideoHooksV2( startIndex: Int?, pageSize: Int?, userHandle: String?, ): ApiResult = hooksService.getUserVideoHooksV2( startIndex = startIndex, pageSize = pageSize, userHandle = userHandle, ).map { response -> response.xAsHooksFeed() } override suspend fun getHooksCarouselContent(): Either = hooksService.getHooksCarouselContent( body = RemoteTabCarouselBody(), ).map { response -> response.xAsTabCarousel() }.onLeft { logger.e(it.toThrowable()) }.mapLeft { Unit } override suspend fun getSingleHookFeed( hookId: Id, ): ApiResult = hooksService.getHook( hookId = hookId.value, ).map { response -> HooksFeed( hooks = listOfNotNull(response.xAsLocalHookData().getOrNull()), ) } override suspend fun incrementShareCount( hookId: Id, recommendationMetadata: RecommendationMetadata, ): ApiResult { val body = RemoteShareHookBody(recommendationMetadata = recommendationMetadata.toRemoteMetadata()) return hooksService.incrementShareCount(hookId.value, body).flatMap { response -> if (response.success == true) { logger.d { "Successfully incremented share count for hook id: ${hookId.value}" } Either.Right(Unit) } else { Either.Left(UnexpectedCallError(Throwable("Error incrementing hook share count"))) } } } override suspend fun getHooksLyrics( hooks: List>, ): Either, Lyrics>> = hooksService.getHooksLyrics( body = RemoteFetchHooksLyricsBody( hookIds = hooks.map { it.value }, ), ).mapLeft { it.toThrowable().toString() }.map { response -> response.hookLyrics ?.mapNotNull { (key, value) -> val lyrics = value.xAsLyrics().getOrElse { error -> logger.d { "failed to parse hooks lyrics for hook $key: $error" } null } ?: return@mapNotNull null Id(key) to lyrics }?.toMap() ?: emptyMap() } override suspend fun getHookDownloadUrl( hookId: Id, ): ApiResult = hooksService.getHookDownloadUrl(hookId.value) .map { it.toHookDownloadResponse() } companion object { private const val PAGE_SIZE = 10 } }