package com.tangem.features.feed.model.search

import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.markets.*
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.domain.search.model.UserAssetSearchItem
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase
import com.tangem.domain.search.usecase.SaveSearchQueryUseCase
import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.feed.components.search.SearchBottomSheetRoute
import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager
import com.tangem.features.feed.model.search.analytics.SearchAnalyticsHelper
import com.tangem.features.feed.model.search.converter.*
import com.tangem.features.feed.model.search.state.SearchStateController
import com.tangem.features.feed.model.search.state.transformers.*
import com.tangem.features.feed.ui.feed.state.MarketChartUM
import com.tangem.features.feed.ui.feed.state.SortChartConfigUM
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
import com.tangem.features.feed.ui.search.state.TextHintItemUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject

private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
private const val MARKET_SEARCH_DEBOUNCE_MS = 500L
private const val RESULTS_SHOWN_DEBOUNCE_MS = 1000L

@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class SearchModel @Inject constructor(
    override val dispatchers: CoroutineDispatcherProvider,
    paramsContainer: ParamsContainer,
    getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
    private val getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
    getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
    getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
    private val getSearchResultsUseCase: GetSearchResultsUseCase,
    private val saveSearchQueryUseCase: SaveSearchQueryUseCase,
    private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase,
    private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase,
    private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
    private val appRouter: AppRouter,
    private val stateController: SearchStateController,
    private val searchAnalyticsHelper: SearchAnalyticsHelper,
) : Model() {

    private val params = paramsContainer.require<DefaultSearchComponent.Params>()

    private val updateQuotesJob = JobHolder()
    private val searchResultsJob = JobHolder()
    private val marketSearchDebounceJob = JobHolder()
    private var shouldShowAllTokensIncludingUnder100k = false
    private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
        maybeAppCurrency.getOrElse { AppCurrency.Default }
    }.stateIn(
        scope = modelScope,
        started = SharingStarted.Eagerly,
        initialValue = AppCurrency.Default,
    )

    private val isBalanceHidden = getBalanceHidingSettingsUseCase.isBalanceHidden()
        .stateIn(
            scope = modelScope,
            started = SharingStarted.Eagerly,
            initialValue = false,
        )

    private val marketsListItemToRecentSearchTokenConverter by lazy {
        MarketsListItemUMToRecentSearchTokenConverter()
    }

    private val recentSearchTokenToMarketsListItemConverter by lazy {
        RecentSearchTokenToMarketsListItemUMConverter()
    }

    private val priceAndTimePointValuesConverter by lazy {
        PriceAndTimePointValuesConverter(shouldFormatAxis = false)
    }

    private val searchMarketsListManager by lazy {
        MarketsListBatchFlowManager(
            getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
            batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
            currentAppCurrency = Provider { currentAppCurrency.value },
            currentTrendInterval = Provider { MarketsListUM.TrendInterval.H24 },
            currentSortByType = Provider { SortByTypeUM.Rating },
            currentSearchText = Provider { stateController.value.searchBar.query },
            modelScope = modelScope,
            dispatchers = dispatchers,
        )
    }

    private val topMarketsManager by lazy {
        FeedMarketsBatchFlowManager(
            getTopFiveMarketTokenUseCase = getTopFiveMarketTokenUseCase,
            currentAppCurrency = Provider { currentAppCurrency.value },
            modelScope = modelScope,
            dispatchers = dispatchers,
            orders = listOf(TokenMarketListConfig.Order.ByRating),
        )
    }

    val bottomSheetNavigation: SlotNavigation<SearchBottomSheetRoute> = SlotNavigation()

    val state: StateFlow<SearchUM> get() = stateController.uiState

    init {
        initCallbacks()
        subscribeToQueryChanges()
        subscribeToMarketUiItems()
        subscribeToQuotesPolling()
        subscribeToAppCurrencyChanges()
        subscribeToMarketLoadingErrors()
        subscribeToResultsShown()
        subscribeToTopMarkets()
        loadHistory()
        searchAnalyticsHelper.sendSearchScreenOpened(params.sourceParams)
    }

    fun loadMore() {
        val content = stateController.value.content
        if (content is SearchContentUM.Results) {
            val market = content.marketTokens
            if (market is MarketSearchResultUM.Content) {
                searchMarketsListManager.loadMore()
            }
        }
    }

    fun clearSearchHistory() {
        modelScope.launch(dispatchers.default) {
            clearSearchHistoryUseCase()
            searchAnalyticsHelper.sendClearButtonClicked()
        }
    }

    fun onTextHintClick(text: String) {
        stateController.update(UpdateSearchBarQueryTransformer(text))
        searchAnalyticsHelper.sendHintClicked(text)
        searchAnalyticsHelper.sendSearchStarted()
    }

    fun onResultMarketTokenClick(item: MarketsListItemUM) {
        modelScope.launch(dispatchers.default) {
            val appCurrency = currentAppCurrency.value
            val input = MarketsListItemUMWithAppCurrency(
                item = item,
                appCurrencyCode = appCurrency.code,
                appCurrencySymbol = appCurrency.symbol,
            )
            saveRecentSearchTokenUseCase(marketsListItemToRecentSearchTokenConverter.convert(input))
            saveSearchQueryUseCase(stateController.value.searchBar.query)
            searchAnalyticsHelper.sendMarketItemClicked(item.currencySymbol)
            withContext(dispatchers.mainImmediate) {
                searchMarketsListManager.getTokenById(item.id)?.let { found ->
                    params.onMarketTokenClick(found.toSerializableParam(), appCurrency)
                }
            }
        }
    }

    fun onHistoryTokenClick(item: MarketsListItemUM) {
        val tokenMarketParams = TokenMarketParams(
            id = item.id,
            name = item.name,
            symbol = item.currencySymbol,
            tokenQuotes = TokenMarketParams.Quotes(
                currentPrice = item.price.fiatPrice,
                h24Percent = null,
                weekPercent = null,
                monthPercent = null,
            ),
            imageUrl = item.iconUrl,
        )
        searchAnalyticsHelper.sendRecentItemClicked(item.currencySymbol)
        params.onMarketTokenClick(tokenMarketParams, currentAppCurrency.value)
    }

    fun onTopMarketSeeAllClick() {
        params.onSeeAllMarketsClick()
    }

    fun onTopMarketItemClick(item: MarketsListItemUM) {
        val token = topMarketsManager.getTokenMarketById(item.id) ?: return
        params.onMarketTokenClick(token.toSerializableParam(), currentAppCurrency.value)
    }

    private fun initCallbacks() {
        stateController.update(object : SearchUMTransformer {
            override fun transform(prevState: SearchUM): SearchUM {
                return prevState.copy(
                    searchBar = prevState.searchBar.copy(
                        onQueryChange = ::onQueryChange,
                        onClearClick = ::onClearClick,
                        onCancelClick = params.onBackClick,
                    ),
                )
            }
        })
    }

    private fun onQueryChange(query: String) {
        stateController.update(UpdateSearchBarQueryTransformer(query))
        searchAnalyticsHelper.sendSearchStarted()
    }

    private fun onClearClick() {
        stateController.update(UpdateSearchBarQueryTransformer(""))
    }

    private fun onSingleUserAssetClick(entry: UserAssetEntry) {
        searchAnalyticsHelper.sendPortfolioItemClicked(entry.currencyStatus.currency.symbol)
        appRouter.push(
            AppRoute.CurrencyDetails(
                userWalletId = entry.userWalletId,
                currency = entry.currencyStatus.currency,
            ),
        )
    }

    private fun onGroupedUserAssetClick(grouped: UserAssetSearchItem.Grouped) {
        searchAnalyticsHelper.sendGroupClicked(grouped.tokenSymbol)
        bottomSheetNavigation.activate(
            SearchBottomSheetRoute.TokenSelector(
                entries = grouped.entries,
                appCurrency = currentAppCurrency.value,
                onTokenSelected = ::onTokenSelectedFromGroup,
                onDismiss = { bottomSheetNavigation.dismiss() },
            ),
        )
    }

    private fun onTokenSelectedFromGroup(entry: UserAssetEntry) {
        bottomSheetNavigation.dismiss()
        onSingleUserAssetClick(entry)
    }

    private fun subscribeToQueryChanges() {
        stateController.uiState
            .map { it.searchBar.query.trim() }
            .distinctUntilChanged()
            .onEach { query ->
                shouldShowAllTokensIncludingUnder100k = false
                if (query.isEmpty()) {
                    marketSearchDebounceJob.cancel()
                    searchMarketsListManager.clearStateAndStopAllActions()
                    updateQuotesJob.cancel()
                    loadHistory()
                } else {
                    stateController.update(SetSearchResultsLoadingTransformer())
                    subscribeToSearchResults(query)
                    modelScope.launch(dispatchers.default) {
                        delay(MARKET_SEARCH_DEBOUNCE_MS)
                        val latestQuery = stateController.value.searchBar.query.trim()
                        if (latestQuery.isNotEmpty()) {
                            searchMarketsListManager.reload(searchText = latestQuery)
                        }
                    }.saveIn(marketSearchDebounceJob)
                }
            }
            .launchIn(modelScope)
    }

    private fun subscribeToSearchResults(query: String) {
        modelScope.launch {
            combine(
                getSearchResultsUseCase(query = query),
                currentAppCurrency,
                isBalanceHidden,
            ) { searchResult, appCurrency, balanceHidden ->
                val converter = UserAssetSearchItemConverter(
                    appCurrency = appCurrency,
                    isBalanceHidden = balanceHidden,
                    onSingleClick = ::onSingleUserAssetClick,
                    onGroupedClick = ::onGroupedUserAssetClick,
                )
                searchResult.userAssets
                    .map(converter::convert)
                    .toImmutableList()
            }.collectLatest { userAssets ->
                stateController.update(UpdateUserAssetsTransformer(userAssets))
            }
        }.saveIn(searchResultsJob)
    }

    private fun subscribeToQuotesPolling() {
        searchMarketsListManager.onLastBatchLoadedSuccess.onEach { batchKey ->
            searchMarketsListManager.loadCharts(setOf(batchKey), MarketsListUM.TrendInterval.H24)
            modelScope.loadQuotesWithTimer(UPDATE_QUOTES_TIMER_MILLIS)
        }.launchIn(modelScope)
    }

    private fun subscribeToAppCurrencyChanges() {
        currentAppCurrency.drop(1).onEach {
            val query = stateController.value.searchBar.query
            if (query.isNotEmpty()) {
                searchMarketsListManager.reload()
            }
        }.launchIn(modelScope)
    }

    private fun subscribeToMarketLoadingErrors() {
        searchMarketsListManager.initialLoadingError
            .onEach { throwable ->
                val (code, message) = when (throwable) {
                    is ApiResponseError.HttpException ->
                        throwable.code.numericCode to throwable.message.orEmpty()
                    else -> null to throwable.message.orEmpty()
                }
                searchAnalyticsHelper.sendErrorMarketsData(code, message)
            }
            .launchIn(modelScope)
    }

    @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
    private fun subscribeToResultsShown() {
        stateController.uiState
            .map { it.searchBar.query.trim() }
            .distinctUntilChanged()
            .flatMapLatest { query ->
                if (query.isEmpty()) {
                    emptyFlow()
                } else {
                    combine(
                        searchMarketsListManager.totalCount.filterNotNull(),
                        stateController.uiState.map { state ->
                            (state.content as? SearchContentUM.Results)?.userAssets?.size ?: 0
                        }.distinctUntilChanged(),
                    ) { marketCount, userAssetsCount ->
                        marketCount to userAssetsCount
                    }
                        .debounce(RESULTS_SHOWN_DEBOUNCE_MS)
                        .take(1)
                }
            }
            .onEach { (marketCount, userAssetsCount) ->
                searchAnalyticsHelper.sendResultShown(
                    totalResultsCount = marketCount + userAssetsCount,
                    marketsResultsCount = marketCount,
                    userTokensResultsCount = userAssetsCount,
                )
            }
            .launchIn(modelScope)
    }

    private fun subscribeToMarketUiItems() {
        combine(
            flow = stateController.uiState.map { it.searchBar.query }.distinctUntilChanged(),
            flow2 = searchMarketsListManager.uiItems,
            flow3 = searchMarketsListManager.isSearchNotFoundState,
            flow4 = searchMarketsListManager.isInInitialLoadingErrorState,
        ) { query, uiItems, isSearchNotFound, isInErrorState ->
            if (query.isEmpty()) return@combine null
            val marketResult = when {
                isSearchNotFound -> MarketSearchResultUM.NotFound
                isInErrorState -> MarketSearchResultUM.NotFound
                uiItems.isEmpty() -> MarketSearchResultUM.Empty
                else -> buildMarketContent(uiItems)
            }
            SearchMarketBatchUiSnapshot(
                marketResult = marketResult,
                isSearchNotFound = isSearchNotFound,
                isInErrorState = isInErrorState,
            )
        }
            .filterNotNull()
            .distinctUntilChanged()
            .onEach { snapshot ->
                stateController.update(ApplySearchMarketBatchTransformer(snapshot))
            }
            .launchIn(modelScope)
    }

    private fun buildMarketContent(allItems: ImmutableList<MarketsListItemUM>): MarketSearchResultUM.Content {
        if (shouldShowAllTokensIncludingUnder100k) {
            return MarketSearchResultUM.Content(items = allItems)
        }

        val filtered = allItems.filter { !it.isUnder100kMarketCap }.toImmutableList()
        val hasUnder100k = filtered.size != allItems.size

        return if (hasUnder100k) {
            MarketSearchResultUM.Content(
                items = filtered,
                shouldShowUnder100kNotification = true,
                onShowUnder100kClick = {
                    shouldShowAllTokensIncludingUnder100k = true
                    stateController.update(
                        UpdateMarketItemsTransformer(
                            MarketSearchResultUM.Content(items = allItems),
                        ),
                    )
                },
            )
        } else {
            MarketSearchResultUM.Content(items = allItems)
        }
    }

    private fun subscribeToTopMarkets() {
        combine(
            topMarketsManager.itemsByOrder,
            topMarketsManager.loadingStatesByOrder,
            topMarketsManager.errorStatesByOrder,
        ) { itemsByOrder, _, errorsByOrder ->
            val ratingItems = itemsByOrder[SortByTypeUM.Rating] ?: persistentListOf()
            val error = errorsByOrder[SortByTypeUM.Rating]
            when {
                ratingItems.isNotEmpty() -> MarketChartUM.Content(
                    items = ratingItems,
                    sortChartConfig = SortChartConfigUM(sortByType = SortByTypeUM.Rating, isSelected = true),
                )
                error != null -> MarketChartUM.LoadingError(
                    onRetryClicked = { topMarketsManager.reloadManager(TokenMarketListConfig.Order.ByRating) },
                )
                else -> MarketChartUM.Loading
            }
        }
            .distinctUntilChanged()
            .onEach { chart ->
                stateController.update(SetTopMarketsTransformer(chart))
                if (chart is MarketChartUM.Content) {
                    topMarketsManager.loadCharts(TokenMarketListConfig.Order.ByRating)
                }
            }
            .launchIn(modelScope)
    }

    private fun loadHistory() {
        modelScope.launch {
            getSearchResultsUseCase(query = "").collectLatest { searchResult ->
                val textHints = searchResult.textHints.map { hint ->
                    TextHintItemUM(text = hint.text)
                }.toImmutableList()

                val appCurrency = currentAppCurrency.value
                val recentTokens = searchResult.recentTokens.map { token ->
                    recentSearchTokenToMarketsListItemConverter.convert(
                        RecentSearchTokenWithAppCurrency(token = token, appCurrency = appCurrency),
                    )
                }.toImmutableList()

                stateController.update(UpdateHistoryTransformer(textHints, recentTokens))

                loadRecentTokenCharts(recentTokens, appCurrency)
            }
        }.saveIn(searchResultsJob)
    }

    private suspend fun loadRecentTokenCharts(tokens: ImmutableList<MarketsListItemUM>, appCurrency: AppCurrency) {
        coroutineScope {
            tokens.map { token ->
                async(dispatchers.io) {
                    val chart = getTokenPriceChartUseCase(
                        appCurrency = appCurrency,
                        interval = PriceChangeInterval.H24,
                        tokenId = token.id,
                        tokenSymbol = token.currencySymbol,
                        preview = true,
                    ).getOrElse { return@async }

                    val chartData = priceAndTimePointValuesConverter.convert(
                        MarketChartData.Data(
                            y = chart.priceY.toImmutableList(),
                            x = chart.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
                        ).sorted(),
                    )

                    stateController.update(UpdateRecentTokenChartTransformer(token.id, chartData))
                }
            }.awaitAll()
        }
    }

    private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
        launch {
            while (true) {
                delay(timeMillis)
                searchMarketsListManager.updateQuotes()
            }
        }.saveIn(updateQuotesJob)
    }
}