package com.tangem.feature.wallet.presentation.wallet.analytics.utils

import arrow.core.getOrElse
import com.google.firebase.perf.FirebasePerformance
import com.google.firebase.perf.metrics.Trace
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase
import com.tangem.domain.analytics.model.WalletBalanceState
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.math.BigDecimal
import javax.inject.Inject

@ModelScoped
internal class TokenListAnalyticsSender @Inject constructor(
    private val analyticsEventHandler: AnalyticsEventHandler,
    private val checkIsWalletToppedUpUseCase: CheckIsWalletToppedUpUseCase,
    private val screenLifecycleProvider: ScreenLifecycleProvider,
) {

    private val balanceWasSentMap = mutableMapOf<String, Boolean>()
    private val yieldPromoShownMap = mutableMapOf<String, Boolean>()
    private val mutex = Mutex()
    private val loadingTraces = mutableMapOf<UserWalletId, Trace>()

    suspend fun send(
        displayedUiState: WalletState?,
        userWallet: UserWallet,
        totalFiatBalance: TotalFiatBalance,
        flattenCurrencies: List<CryptoCurrencyStatus>,
    ) {
        if (screenLifecycleProvider.isBackgroundState.value) return
        if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return

        val isFlickering = totalFiatBalance is TotalFiatBalance.Loaded && totalFiatBalance.source == StatusSource.CACHE
        if (totalFiatBalance is TotalFiatBalance.Loading || isFlickering) {
            startLoadingTraceIfNeeded(userWallet.walletId, flattenCurrencies)
            return
        }

        if (isTerminalState(totalFiatBalance)) {
            stopLoadingTraceIfNeeded(userWallet.walletId, totalFiatBalance)
        }

        sendBalanceLoadedEventIfNeeded(totalFiatBalance, flattenCurrencies)
        sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, flattenCurrencies)
        sendUnreachableNetworksEventIfNeeded(flattenCurrencies)
        sendTokenBalancesIfNeeded(flattenCurrencies)
    }

    private suspend fun startLoadingTraceIfNeeded(
        userWalletId: UserWalletId,
        flattenCurrencies: List<CryptoCurrencyStatus>,
    ) {
        mutex.withLock {
            if (!loadingTraces.containsKey(userWalletId)) {
                val trace = FirebasePerformance.getInstance().newTrace(BALANCE_LOADED_TRACE_NAME)
                trace.start()
                trace.putAttribute(TOKENS_COUNT, flattenCurrencies.size.toString())
                loadingTraces[userWalletId] = trace
            }
        }
    }

    private suspend fun stopLoadingTraceIfNeeded(userWalletId: UserWalletId, totalFiatBalance: TotalFiatBalance) {
        mutex.withLock {
            loadingTraces[userWalletId]?.apply {
                when (totalFiatBalance) {
                    is TotalFiatBalance.Loaded -> putAttribute(HAS_ERROR, "No")
                    is TotalFiatBalance.Failed -> putAttribute(HAS_ERROR, "Yes")
                    else -> { /* Intentionally do nothing */
                    }
                }
                stop()
                loadingTraces.remove(userWalletId)
            }
        }
    }

    private fun isTerminalState(balance: TotalFiatBalance): Boolean {
        val isLoaded = balance is TotalFiatBalance.Loaded &&
            (balance.source == StatusSource.ACTUAL || balance.source == StatusSource.ONLY_CACHE)
        return balance is TotalFiatBalance.Failed || isLoaded
    }

    private fun sendBalanceLoadedEventIfNeeded(
        fiatBalance: TotalFiatBalance,
        currenciesStatuses: List<CryptoCurrencyStatus>,
    ) {
        createCardBalanceState(fiatBalance, currenciesStatuses)?.let { balanceState ->
            analyticsEventHandler.send(
                Basic.BalanceLoaded(
                    balance = balanceState,
                    tokensCount = currenciesStatuses.size,
                ),
            )
        }
    }

    private fun createCardBalanceState(
        fiatBalance: TotalFiatBalance,
        currenciesStatuses: List<CryptoCurrencyStatus>,
    ): AnalyticsParam.CardBalanceState? {
        return when (fiatBalance) {
            is TotalFiatBalance.Failed -> getCardBalanceState(currenciesStatuses)
            is TotalFiatBalance.Loaded -> getCardBalanceState(fiatBalance)
            is TotalFiatBalance.Loading -> null
        }
    }

    private fun getCardBalanceState(currenciesStatuses: List<CryptoCurrencyStatus>): AnalyticsParam.CardBalanceState {
        return when {
            currenciesStatuses.isEmpty() -> AnalyticsParam.CardBalanceState.Empty
            currenciesStatuses.any { it.value is CryptoCurrencyStatus.NoQuote } -> {
                AnalyticsParam.CardBalanceState.NoRate
            }
            else -> AnalyticsParam.CardBalanceState.BlockchainError
        }
    }

    private suspend fun sendTokenBalancesIfNeeded(currenciesStatuses: List<CryptoCurrencyStatus>) {
        currenciesStatuses.forEach {
            val status = it.value
            if (status is CryptoCurrencyStatus.Loaded) {
                sendTokenBalancesForSpecificBlockchains(it, status)
            }
        }
    }

    // TODO hotfix/5.7.4 send event for log if tokens from polkadot ecosystem have balance
    private suspend fun sendTokenBalancesForSpecificBlockchains(
        currencyStatus: CryptoCurrencyStatus,
        balanceStatus: CryptoCurrencyStatus.Loaded,
    ) {
        // for now send only for Polkadot ecosystem blockchains
        // later dependency on Blockchain will be removed and use token name
        when (val blockchain = Blockchain.fromNetworkId(currencyStatus.currency.network.rawId)) {
            Blockchain.Polkadot,
            Blockchain.AlephZero,
            Blockchain.Kusama,
            -> {
                if (balanceWasSentMap[blockchain.currency] != true) {
                    val tokenBalance = if (balanceStatus.amount.isZero()) {
                        AnalyticsParam.EmptyFull.Empty
                    } else {
                        AnalyticsParam.EmptyFull.Full
                    }
                    analyticsEventHandler.send(
                        Basic.TokenBalance(
                            balance = tokenBalance,
                            token = blockchain.currency,
                        ),
                    )
                    mutex.withLock {
                        balanceWasSentMap[blockchain.currency] = true
                    }
                }
            }
            else -> {
                /* no-op */
            }
        }
    }

    private fun getCardBalanceState(fiatBalance: TotalFiatBalance.Loaded): AnalyticsParam.CardBalanceState {
        return if (fiatBalance.amount > BigDecimal.ZERO) {
            AnalyticsParam.CardBalanceState.Full
        } else {
            AnalyticsParam.CardBalanceState.Empty
        }
    }

    private suspend fun sendToppedUpEventIfNeeded(
        userWallet: UserWallet,
        fiatBalance: TotalFiatBalance,
        currenciesStatuses: List<CryptoCurrencyStatus>,
    ) {
        val balanceState = getWalletBalanceState(fiatBalance) ?: return

        val isWalletToppedUp = checkIsWalletToppedUpUseCase(userWallet.walletId, balanceState)
            .getOrElse { return }

        if (isWalletToppedUp) {
            val walletType = if (userWallet.isMultiCurrency) {
                AnalyticsParam.WalletType.MultiCurrency
            } else {
                // For single currency wallets with token list, e.g. Noodle
                val currency = currenciesStatuses.firstOrNull() ?: return

                AnalyticsParam.WalletType.SingleCurrency(currency.currency.name)
            }

            analyticsEventHandler.send(
                Basic.ToppedUp(
                    userWalletId = userWallet.walletId.stringValue,
                    walletType = walletType,
                ),
            )
        }
    }

    private fun getWalletBalanceState(fiatBalance: TotalFiatBalance): WalletBalanceState? {
        return when (fiatBalance) {
            is TotalFiatBalance.Failed -> WalletBalanceState.Error
            is TotalFiatBalance.Loaded -> {
                if (fiatBalance.amount > BigDecimal.ZERO) {
                    WalletBalanceState.ToppedUp
                } else {
                    WalletBalanceState.Empty
                }
            }
            is TotalFiatBalance.Loading -> null
        }
    }

    private fun sendUnreachableNetworksEventIfNeeded(currenciesStatuses: List<CryptoCurrencyStatus>) {
        val unreachableCurrencies = currenciesStatuses
            .filter { it.value is CryptoCurrencyStatus.Unreachable }
            .map { it.currency.symbol }

        if (unreachableCurrencies.isNotEmpty()) {
            analyticsEventHandler.send(MainScreen.NetworksUnreachable(unreachableCurrencies))
        }
    }

    fun sendYieldPromoShown(userWalletId: UserWalletId, token: String, blockchain: String) {
        val key = "${userWalletId.stringValue}_${blockchain}_$token"
        if (yieldPromoShownMap[key] == true) return

        analyticsEventHandler.send(
            MainScreenAnalyticsEvent.YieldPromo(
                token = token,
                blockchain = blockchain,
            ),
        )
        yieldPromoShownMap[key] = true
    }

    companion object {
        const val BALANCE_LOADED_TRACE_NAME = "Total_balance_loaded"
        const val HAS_ERROR = "has_error"
        const val TOKENS_COUNT = "tokens_count"
    }
}