package com.tangem.feature.swap

import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
import com.tangem.datasource.api.express.models.response.SwapPair
import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders
import com.tangem.data.common.txhistory.ExpressHistoryRepository
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.ExpressException
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import java.io.IOException
import java.util.UUID
import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo

@Suppress("LongParameterList", "LargeClass")
internal class DefaultSwapRepository(
    private val tangemExpressApi: TangemExpressApi,
    private val coroutineDispatcher: CoroutineDispatcherProvider,
    private val errorsDataConverter: ErrorsDataConverter,
    private val dataSignatureVerifier: DataSignatureVerifier,
    private val appPreferencesStore: AppPreferencesStore,
    private val expressHistoryRepository: ExpressHistoryRepository,
    private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
    moshi: Moshi,
) : SwapRepository {

    private val expressDataConverter = ExpressDataConverter()
    private val leastTokenInfoConverter = LeastTokenInfoConverter()
    private val swapPairInfoConverter = SwapPairInfoConverter()
    private val exchangeStatusConverter = ExchangeStatusConverter(moshi)
    private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)

    override suspend fun getPairs(
        userWallet: UserWallet,
        initialCurrency: LeastTokenInfo,
        currencyList: List<CryptoCurrency>,
    ): PairsWithProviders {
        return withContext(coroutineDispatcher.io) {
            try {
                val initial = NetworkLeastTokenInfo(
                    contractAddress = initialCurrency.contractAddress,
                    network = initialCurrency.network,
                )
                val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) }

                val pairsDeferred = async {
                    getPairsInternal(
                        userWallet = userWallet,
                        from = arrayListOf(initial),
                        to = currenciesList,
                    )
                }

                val reversedPairsDeferred = async {
                    getPairsInternal(
                        userWallet = userWallet,
                        from = currenciesList,
                        to = arrayListOf(initial),
                    )
                }

                val pairs = pairsDeferred.await().getOrThrow()
                val reversedPairs = reversedPairsDeferred.await().getOrThrow()

                val allPairs = pairs + reversedPairs

                val providers = tangemExpressApi.getProviders(
                    userWalletId = userWallet.walletId.stringValue,
                    refCode = ExpressUtils.getRefCode(
                        userWallet = userWallet,
                        appPreferencesStore = appPreferencesStore,
                    ),
                ).getOrThrow()

                return@withContext swapPairInfoConverter.convert(
                    SwapPairsWithProviders(
                        swapPair = allPairs,
                        providers = providers,
                    ),
                )
            } catch (exception: Exception) {
                if (exception is ApiResponseError.HttpException) {
                    throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty()))
                } else {
                    throw exception
                }
            }
        }
    }

    private suspend fun getPairsInternal(
        userWallet: UserWallet,
        from: List<NetworkLeastTokenInfo>,
        to: List<NetworkLeastTokenInfo>,
    ): ApiResponse<List<SwapPair>> {
        return tangemExpressApi.getPairs(
            userWalletId = userWallet.walletId.stringValue,
            refCode = ExpressUtils.getRefCode(
                userWallet = userWallet,
                appPreferencesStore = appPreferencesStore,
            ),
            body = PairsRequestBody(
                from = from,
                to = to,
            ),
        )
    }

    override suspend fun getExchangeStatus(
        userWallet: UserWallet?,
        userWalletId: UserWalletId,
        txId: String,
    ): Either<UnknownError, ExchangeStatusModel> {
        return withContext(coroutineDispatcher.io) {
            either {
                catch(
                    block = {
                        val response = tangemExpressApi
                            .getExchangeStatus(
                                userWalletId = userWalletId.stringValue,
                                refCode = ExpressUtils.getRefCode(
                                    userWallet = userWallet,
                                    appPreferencesStore = appPreferencesStore,
                                ),
                                txId = txId,
                            )
                            .getOrThrow()

                        if (txHistoryFeatureToggles.isNewTxHistoryEnabled) {
                            expressHistoryRepository.storeExchanges(items = listOf(response))
                        }

                        exchangeStatusConverter.convert(response)
                    },
                    catch = { exception ->
                        TangemLogger.e("getExchangeStatus error", exception)
                        raise(UnknownError(exception.message))
                    },
                )
            }
        }
    }

    override suspend fun findBestQuote(
        userWallet: UserWallet,
        fromContractAddress: String,
        fromNetwork: String,
        toContractAddress: String,
        toNetwork: String,
        fromAmount: String,
        fromDecimals: Int,
        toDecimals: Int,
        providerId: String,
        rateType: RateType,
    ): Either<ExpressDataError, QuoteModel> {
        return withContext(coroutineDispatcher.io) {
            try {
                val response = tangemExpressApi.getExchangeQuote(
                    fromContractAddress = fromContractAddress,
                    fromNetwork = fromNetwork,
                    toContractAddress = toContractAddress,
                    toNetwork = toNetwork,
                    fromAmount = fromAmount,
                    fromDecimals = fromDecimals,
                    toDecimals = toDecimals,
                    providerId = providerId,
                    rateType = rateType.name.lowercase(),
                    userWalletId = userWallet.walletId.stringValue,
                    refCode = ExpressUtils.getRefCode(
                        userWallet = userWallet,
                        appPreferencesStore = appPreferencesStore,
                    ),
                ).getOrThrow()
                QuoteModel(
                    toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
                    allowanceContract = response.allowanceContract,
                    txType = response.txType?.toDomain(),
                ).right()
            } catch (ex: Exception) {
                getDataError(ex).left()
            }
        }
    }

    override suspend fun getExchangeData(
        userWallet: UserWallet,
        fromContractAddress: String,
        fromNetwork: String,
        toContractAddress: String,
        fromAddress: String,
        toNetwork: String,
        fromAmount: String,
        fromDecimals: Int,
        toDecimals: Int,
        providerId: String,
        rateType: RateType,
        toAddress: String,
        expressOperationType: ExpressOperationType,
        refundAddress: String?, // for cex only
        refundExtraId: String?, // for cex only
        toExtraId: String?, // for networks with memo only
    ): Either<ExpressDataError, SwapDataModel> {
        return withContext(coroutineDispatcher.io) {
            try {
                val requestId = UUID.randomUUID().toString()
                val response = tangemExpressApi.getExchangeData(
                    fromContractAddress = fromContractAddress,
                    fromNetwork = fromNetwork,
                    toContractAddress = toContractAddress,
                    fromAddress = fromAddress,
                    toNetwork = toNetwork,
                    fromAmount = fromAmount,
                    fromDecimals = fromDecimals,
                    toDecimals = toDecimals,
                    providerId = providerId,
                    rateType = rateType.name.lowercase(),
                    toAddress = toAddress,
                    requestId = requestId,
                    refundAddress = refundAddress,
                    refundExtraId = refundExtraId,
                    partnerOperationType = expressOperationType.value,
                    userWalletId = userWallet.walletId.stringValue,
                    refCode = ExpressUtils.getRefCode(
                        userWallet = userWallet,
                        appPreferencesStore = appPreferencesStore,
                    ),
                    toExtraId = toExtraId?.ifEmpty { null },
                    quoteId = null, // TODO add when implementing fixed rate in swap
                ).getOrThrow()
                if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
                    val txDetails = parseTxDetails(response.txDetailsJson)
                        ?: return@withContext ExpressDataError.UnknownError().left()
                    if (txDetails.requestId != requestId) {
                        return@withContext ExpressDataError.InvalidRequestIdError().left()
                    }
                    if (!toAddress.equals(txDetails.payoutAddress, ignoreCase = true)) {
                        return@withContext ExpressDataError.InvalidPayoutAddressError().left()
                    }
                    expressDataConverter.convert(
                        ExchangeDataResponseWithTxDetails(
                            dataResponse = response,
                            txDetails = txDetails,
                        ),
                    ).right()
                } else {
                    ExpressDataError.InvalidSignatureError().left()
                }
            } catch (ex: Exception) {
                getDataError(ex).left()
            }
        }
    }

    override suspend fun exchangeSent(
        userWallet: UserWallet,
        txId: String,
        fromNetwork: String,
        fromAddress: String,
        payInAddress: String,
        txHash: String,
        payInExtraId: String?,
    ): Either<ExpressDataError, Unit> = withContext(coroutineDispatcher.io) {
        try {
            tangemExpressApi.exchangeSent(
                userWalletId = userWallet.walletId.stringValue,
                refCode = ExpressUtils.getRefCode(
                    userWallet = userWallet,
                    appPreferencesStore = appPreferencesStore,
                ),
                body = ExchangeSentRequestBody(
                    txId = txId,
                    fromNetwork = fromNetwork,
                    fromAddress = fromAddress,
                    payinAddress = payInAddress,
                    payinExtraId = payInExtraId,
                    txHash = txHash,
                ),
            ).getOrThrow()
            Unit.right()
        } catch (ex: Exception) {
            getDataError(ex).left()
        }
    }

    private fun parseTxDetails(txDetailsJson: String): TxDetails? {
        return try {
            txDetailsMoshiAdapter.fromJson(txDetailsJson)
        } catch (e: IOException) {
            TangemLogger.e("error parsing txDetailsJson", e)
            null
        }
    }

    private fun getDataError(ex: Exception): ExpressDataError {
        return if (ex is ApiResponseError.HttpException) {
            errorsDataConverter.convert(ex.errorBody.orEmpty())
        } else {
            ExpressDataError.UnknownError()
        }
    }

    override suspend fun getStoredSwapUiMode(): SwapUIMode? {
        return appPreferencesStore.getObjectSyncOrNull(PreferencesKeys.SWAP_UI_MODE_KEY)
    }

    override suspend fun storeSwapUiMode(mode: SwapUIMode) {
        appPreferencesStore.storeObject(PreferencesKeys.SWAP_UI_MODE_KEY, mode)
    }
}