# ArticlesRepository.kt

    /*
     * The MIT License
     *
     * Original work sponsored and donated by The Danish Health Data Authority (http://www.sundhedsdatastyrelsen.dk)
     *
     * Copyright (C) 2025 The Danish Health Data Authority (http://www.sundhedsdatastyrelsen.dk)
     *
     *
     * Permission is hereby granted, free of charge, to any person obtaining a copy of
     * this software and associated documentation files (the "Software"), to deal in
     * the Software without restriction, including without limitation the rights to
     * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
     * of the Software, and to permit persons to whom the Software is furnished to do
     * so, subject to the following conditions:
     *
     *
     * The above copyright notice and this permission notice shall be included in all
     * copies or substantial portions of the Software.
     *
     *
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     * SOFTWARE.
     */
    package dk.nsp.mingraviditet.core.data.repository.knowledge

    import dk.nsp.mingraviditet.core.data.domain.articles.ArticleDomainModel
    import dk.nsp.mingraviditet.core.data.domain.articles.ArticleRefDomainModel
    import dk.nsp.mingraviditet.core.data.domain.articles.OverviewArticles
    import dk.nsp.mingraviditet.core.data.domain.articles.toDomain
    import dk.nsp.mingraviditet.core.data.domain.pregnancyInfo.PregnancyState
    import dk.nsp.mingraviditet.core.data.repository.overview.PregnancyRepository
    import dk.nsp.mingraviditet.core.data.util.domainModel
    import dk.nsp.mingraviditet.core.model.data.ArticleCategory
    import dk.nsp.mingraviditet.core.network.articles.ArticlesDataSource
    import dk.nsp.mingraviditet.core.network.articles.model.ArticleSectionDTO
    import dk.nsp.mingraviditet.core.network.articles.model.RecommendedArticlesDTO
    import dk.nsp.mingraviditet.core.network.articles.model.asExternalModel
    import dk.nsp.mingraviditet.core.shared.network.Dispatcher
    import dk.nsp.mingraviditet.core.shared.network.Failed
    import dk.nsp.mingraviditet.core.shared.network.Loading
    import dk.nsp.mingraviditet.core.shared.network.MGDispatchers
    import dk.nsp.mingraviditet.core.shared.network.ResultWrapper
    import dk.nsp.mingraviditet.core.shared.network.Success
    import dk.nsp.mingraviditet.core.shared.network.mapSuccessWithTryOrFailed
    import dk.nsp.mingraviditet.core.shared.network.toSuccess
    import kotlinx.coroutines.CoroutineDispatcher
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.Deferred
    import kotlinx.coroutines.async
    import kotlinx.coroutines.flow.Flow
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.combine
    import kotlinx.coroutines.flow.flow
    import kotlinx.coroutines.flow.flowOn
    import kotlinx.coroutines.withContext
    import java.time.LocalDate
    import javax.inject.Inject
    import javax.inject.Singleton
    import kotlin.random.Random

    @Singleton
    class ArticlesRepository @Inject constructor(
        private val articlesDataSource: ArticlesDataSource,
        pregnancyRepository: PregnancyRepository,
        @Dispatcher(MGDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
    ) {
        private val _refreshTrigger = MutableStateFlow(0)
        private val cachedCategories = MutableStateFlow<List<ArticleCategory>?>(null)

        val categories = flow {
            cachedCategories.value?.let {
                emit(it.toSuccess())
                return@flow
            }
            emit(Loading)
            val result = withContext(ioDispatcher) {
                getCategories()
            }.await()

            emit(result)
        }

        val overviewArticles: Flow<ResultWrapper<OverviewArticles?>> =
            combine(
                pregnancyRepository.pregnancyInfo,
                _refreshTrigger,
            ) { pregnancyInfo, _ ->
                withContext(ioDispatcher) {
                    getCategories()
                }
                when (pregnancyInfo) {
                    is Failed, is Loading -> {
                        return@combine pregnancyInfo
                    }

                    is Success -> {
                        val state: PregnancyState = pregnancyInfo.value.pregnancyState
                        withContext(ioDispatcher) {
                            getArticlesFromState(state).await()
                        }
                    }
                }
            }.flowOn(
                ioDispatcher,
            )

        fun refreshArticles() {
            _refreshTrigger.value += 1
        }

        fun CoroutineScope.getArticlesFromState(pregnancyState: PregnancyState): Deferred<ResultWrapper<OverviewArticles?>> =
            async {
                when (pregnancyState) {
                    is PregnancyState.GaveBirth -> {
                        getAfterBirthArticles(pregnancyState.dateOfBirth).await()
                    }

                    PregnancyState.NoPregnancyDataAvailable, PregnancyState.ShowSofaView -> {
                        Success(null)
                    }

                    PregnancyState.SetDueDate -> {
                        getOverviewArticles(null).await()
                    }

                    is PregnancyState.WaitingForBirth -> {
                        getOverviewArticles(pregnancyState.dueDate).await()
                    }
                }
            }

        fun CoroutineScope.getOverviewArticles(dueDate: LocalDate?): Deferred<ResultWrapper<OverviewArticles>> =
            async(ioDispatcher) {
                with(articlesDataSource) {
                    val result: ResultWrapper<RecommendedArticlesDTO> = if (dueDate != null) {
                        getBeforeBirthArticlesFromDueDate(dueDate).await()
                    } else {
                        // TODO: Random week number is intentional for now, but will be changed later when we have a way to identify relevant articles for anonymous user without a due date
                        getBeforeBirthArticlesFromWeek(weekNumber = Random.nextInt(0, 11)).await()
                    }
                    when (result) {
                        is Failed, is Loading -> return@async result
                        is Success<RecommendedArticlesDTO> -> return@async result.mapToOverviewArticles(
                            includeNextWeek = dueDate != null,
                        )
                    }
                }
            }

        private fun ResultWrapper<RecommendedArticlesDTO>.mapToOverviewArticles(includeNextWeek: Boolean = true): ResultWrapper<OverviewArticles> =
            mapSuccessWithTryOrFailed { recommendedArticles: RecommendedArticlesDTO ->
                OverviewArticles(
                    currentWeek = recommendedArticles.currentWeek.map { currentWeekSection: ArticleSectionDTO ->
                        currentWeekSection.asExternalModel().domainModel()
                    },
                    nextWeek = if (includeNextWeek) {
                        recommendedArticles.nextWeek?.map { nextWeekSection: ArticleSectionDTO ->
                            nextWeekSection.asExternalModel().domainModel()
                        }
                    } else {
                        null
                    },
                )
            }

        fun CoroutineScope.getAfterBirthArticles(birthDate: LocalDate) = async(ioDispatcher) {
            with(articlesDataSource) {
                val result = getAfterBirthArticles(birthDate).await()
                when (result) {
                    is Failed, is Loading -> result
                    is Success -> result.mapToOverviewArticles()
                }
            }
        }

        private fun CoroutineScope.getCategories(): Deferred<ResultWrapper<List<ArticleCategory>>> =
            async(ioDispatcher) {
                with(articlesDataSource) {
                    val result = getCategories().await()
                    when (result) {
                        is Failed, is Loading -> result
                        is Success -> {
                            val categories = result.value.map { it.asExternalModel() }
                            cachedCategories.value = categories
                            categories.toSuccess()
                        }
                    }
                }
            }

        fun CoroutineScope.search(
            query: String,
        ): Deferred<ResultWrapper<List<ArticleDomainModel>>> = async(ioDispatcher) {
            val searchResponse = with(
                articlesDataSource,
            ) {
                searchArticles(query)
            }.await()

            searchResponse.mapSuccessWithTryOrFailed {
                it.sections.map {
                    it.asExternalModel().domainModel()
                }
            }
        }

        fun CoroutineScope.getArticleBySectionDocumentId(sectionDocumentId: String): Deferred<ResultWrapper<ArticleRefDomainModel?>> = async(ioDispatcher) {
            val articleResponse = with(
                articlesDataSource,
            ) {
                getArticleBySectionDocumentId(sectionDocumentId)
            }.await()

            articleResponse.mapSuccessWithTryOrFailed {
                it.toDomain()
            }
        }

        fun CoroutineScope.getArticleByArticleDocumentId(articleDocumentId: String): Deferred<ResultWrapper<ArticleRefDomainModel?>> = async(ioDispatcher) {
            val articleResponse = with(
                articlesDataSource,
            ) {
                getArticleByArticleDocumentId(articleDocumentId)
            }.await()

            articleResponse.mapSuccessWithTryOrFailed {
                it.toDomain()
            }
        }
    }