package com.tangem.features.managetokens.model

import arrow.core.getOrElse
import com.tangem.utils.logging.TangemLogger
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.managetokens.CheckDerivationPathSupportedUseCase
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
import com.tangem.domain.managetokens.FindTokenUseCase
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.CustomCurrencyFormBuilder
import com.tangem.features.managetokens.utils.CustomCurrencyValidator
import com.tangem.features.managetokens.utils.list.CustomTokenFormUseCasesFacade
import com.tangem.features.managetokens.utils.mapper.mapToDomainModel
import com.tangem.features.managetokens.utils.ui.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.mutate
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject

// TODO: Divide to sub-components: [REDACTED_JIRA]
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class CustomTokenFormModel @Inject constructor(
    override val dispatchers: CoroutineDispatcherProvider,
    private val messageSender: UiMessageSender,
    private val customTokenFormManager: CustomCurrencyFormBuilder,
    private val analyticsEventHandler: AnalyticsEventHandler,
    private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
    private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
    private val findTokenUseCase: FindTokenUseCase,
    private val validateTokenFormUseCase: ValidateTokenFormUseCase,
    private val checkDerivationPathSupportedUseCase: CheckDerivationPathSupportedUseCase,
    paramsContainer: ParamsContainer,
    customTokenFormUseCasesFacadeFactory: CustomTokenFormUseCasesFacade.Factory,
) : Model() {

    private val params: CustomTokenFormComponent.Params = paramsContainer.require()
    private var createdCurrency: CryptoCurrency? = null
    private val useCasesFacade: CustomTokenFormUseCasesFacade =
        customTokenFormUseCasesFacadeFactory.create(params.mode.userWalletId)
    private val customCurrencyValidator = CustomCurrencyValidator(
        userWalletId = params.mode.userWalletId,
        useCasesFacade = useCasesFacade,
        createCryptoCurrencyUseCase = createCryptoCurrencyUseCase,
        findTokenUseCase = findTokenUseCase,
        validateTokenFormUseCase = validateTokenFormUseCase,
    )

    val state: MutableStateFlow<CustomTokenFormUM> = MutableStateFlow(
        value = getInitialState(),
    )

    init {
        observeValidatorUpdates()

        if (params.network.canHandleTokens) {
            observeTokenFormUpdates()
        }

        validatePrefilledForm()
    }

    private fun getInitialState(): CustomTokenFormUM {
        return CustomTokenFormUM(
            networkName = ClickableFieldUM(
                label = resourceReference(R.string.custom_token_network_input_title),
                value = stringReference(params.network.name),
                onClick = ::selectNetwork,
            ),
            tokenForm = if (params.network.canHandleTokens) {
                customTokenFormManager.buildForm(
                    updateFormFieldValue = ::updateFormFieldValue,
                    updateFormFieldFocus = ::updateFormFieldFocus,
                )
            } else {
                null
            },
            derivationPath = getDerivationPathFormIfSupported(),
            saveToken = ::addCurrency,
        )
    }

    private fun getDerivationPathFormIfSupported(): ClickableFieldUM? {
        // If network doesn't support derivation path, return null
        if (params.network.derivationPath is Network.DerivationPath.None) return null

        return ClickableFieldUM(
            label = resourceReference(R.string.custom_token_derivation_path),
            value = if (params.derivationPath == null || params.derivationPath.isDefault) {
                resourceReference(R.string.custom_token_derivation_path_default)
            } else {
                stringReference(params.derivationPath.name)
            },
            onClick = ::selectDerivationPath,
        )
    }

    @OptIn(FlowPreview::class)
    private fun observeTokenFormUpdates() {
        state
            .transform { state ->
                val form = state.tokenForm

                if (form != null && !form.wasFilled) {
                    emit(form.mapToDomainModel())
                }
            }
            .distinctUntilChanged()
            .sample(periodMillis = 1_000)
            .drop(count = 1) // Skip initial state
            .onEach { formValues ->
                customCurrencyValidator.validateForm(
                    networkId = params.network.id,
                    derivationPath = getDerivationPath(),
                    formValues = formValues,
                )
            }
            .launchIn(modelScope)
    }

    private fun observeValidatorUpdates() = modelScope.launch {
        customCurrencyValidator.consumeUpdates { validatorState ->
            createdCurrency = null

            when (validatorState) {
                is CustomCurrencyValidator.Status.NotStarted,
                is CustomCurrencyValidator.Status.Validating,
                -> Unit
                is CustomCurrencyValidator.Status.SearchingToken -> updateStateWithProgress()
                is CustomCurrencyValidator.Status.UnexpectedException -> showErrorDialog(validatorState.cause)
                is CustomCurrencyValidator.Status.FormValidationException -> updateStateWithExceptions(
                    exceptions = validatorState.exceptions,
                )
                is CustomCurrencyValidator.Status.TokenNotFound -> updateStateWithNotFoundNotification()
                is CustomCurrencyValidator.Status.Validated -> {
                    createdCurrency = validatorState.currency

                    updateStateWithCurrency(
                        currency = validatorState.currency,
                        fillForm = validatorState.fillForm,
                        isAlreadyAdded = validatorState.isAlreadyAdded,
                        isCustom = validatorState.isCustom,
                    )
                }
            }
        }
    }

    private fun validatePrefilledForm() = modelScope.launch {
        customCurrencyValidator.validateForm(
            networkId = params.network.id,
            derivationPath = getDerivationPath(),
            formValues = state.value.tokenForm.mapToDomainModel(),
        )
    }

    private fun updateStateWithCurrency(
        currency: CryptoCurrency,
        fillForm: Boolean,
        isAlreadyAdded: Boolean,
        isCustom: Boolean,
    ) = modelScope.launch {
        val isNeedColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke(
            userWalletId = params.mode.userWalletId,
            networksWithDerivationPath = mapOf(currency.network.rawId to getDerivationPath().value),
        )

        state.update { state ->
            var updatedState = state
                .updateWithProgress(
                    showProgress = false,
                    canAddToken = !isAlreadyAdded,
                    isWasFilled = fillForm,
                    clearNotifications = true,
                    clearFieldErrors = true,
                    disableSecondaryFields = !isCustom,
                    walletInteractionIcon = R.drawable.ic_tangem_24.takeIf { isNeedColdWalletInteraction },
                )

            if (fillForm) {
                updatedState = updatedState.updateWithCurrency(currency)
            }

            if (isAlreadyAdded) {
                updatedState = updatedState.updateWithCurrencyAlreadyAddedNotification()
            }

            if (isCustom) {
                updatedState = updatedState.updateWithCurrencyNotFoundNotification()
            }

            updatedState
        }
    }

    private fun updateStateWithNotFoundNotification() {
        state.update { state ->
            state
                .updateWithProgress(
                    showProgress = false,
                    canAddToken = false,
                    clearNotifications = true,
                    clearFieldErrors = true,
                )
                .updateWithCurrencyNotFoundNotification()
        }
    }

    private fun updateStateWithProgress() {
        state.update { state ->
            state.updateWithProgress(
                showProgress = true,
                canAddToken = false,
                clearNotifications = false,
                clearFieldErrors = false,
            )
        }
    }

    private fun showDerivationPathNotSupportedDialog() {
        val dialog = DialogMessage(
            message = resourceReference(R.string.custom_token_invalid_derivation_path),
        )

        messageSender.send(dialog)
    }

    private fun showErrorDialog(throwable: Throwable) {
        TangemLogger.e("Error", throwable)
        val message = when (throwable) {
            is TangemSdkError -> resourceReference(
                R.string.generic_error_code,
                wrappedList(throwable.code.toString()),
            )
            else -> resourceReference(R.string.common_unknown_error)
        }
        val dialog = DialogMessage(
            message = message,
        )

        messageSender.send(dialog)
    }

    private fun updateStateWithExceptions(exceptions: List<CustomTokenFormValidationException>) {
        state.update { state ->
            val validatedState = state
                .updateWithProgress(
                    showProgress = false,
                    canAddToken = false,
                    clearNotifications = true,
                    clearFieldErrors = true,
                )

            exceptions.fold(validatedState) { stateAcc, exception ->
                when (exception) {
                    is CustomTokenFormValidationException.ContractAddress -> {
                        stateAcc.updateWithContractAddressException(exception)
                    }
                    is CustomTokenFormValidationException.Decimals -> {
                        stateAcc.updateWithDecimalsException(exception)
                    }
                    is CustomTokenFormValidationException.EmptyName -> {
                        stateAcc // No need to display error
                    }
                    is CustomTokenFormValidationException.EmptySymbol -> {
                        stateAcc // No need to display error
                    }
                    is CustomTokenFormValidationException.DataError -> {
                        TangemLogger.e("Unable to validate custom currency", exception.cause)
                        stateAcc
                    }
                }
            }
        }
    }

    private fun getDerivationPath(): Network.DerivationPath {
        return params.derivationPath?.value ?: params.network.derivationPath
    }

    private fun updateFormFieldValue(field: Field, value: String) {
        state.update { state ->
            state.updateTokenForm {
                val fieldValue = fields.getValue(field)

                if (!fieldValue.isEnabled) return@updateTokenForm this

                val updatedFieldValue = fieldValue.copy(
                    value = value,
                )
                val updatedFields = fields.mutate {
                    it[field] = updatedFieldValue
                }

                copy(
                    fields = updatedFields,
                    wasFilled = false,
                )
            }
        }
    }

    private fun updateFormFieldFocus(field: Field, isFocused: Boolean) {
        state.update { state ->
            state.updateTokenForm {
                val fieldValue = fields.getValue(field)

                if (!fieldValue.isEnabled) return@updateTokenForm this

                val updatedFieldValue = fieldValue.copy(
                    isFocused = isFocused,
                )
                val updatedFields = fields.mutate {
                    it[field] = updatedFieldValue
                }

                // Checking if a field is out of focus
                if (fieldValue.isFocused && !isFocused && fieldValue.value.isNotEmpty()) {
                    sendFieldAnalyticsEvent(field, fieldValue)
                }

                copy(fields = updatedFields)
            }
        }
    }

    private fun addCurrency() = resource(
        acquire = {
            state.update { state ->
                state.updateWithProgress(showProgress = true)
            }
        },
        release = {
            state.update { state ->
                state.updateWithProgress(showProgress = false)
            }
        },
    ) {
        val currency = createdCurrency
        if (currency == null) {
            showErrorDialog(IllegalStateException("Trying to add currency without validation"))
            return@resource
        }

        val isDerivationSupported = checkDerivationPathSupportedUseCase(
            userWalletId = params.mode.userWalletId,
            networkId = currency.network.id,
            derivationPath = currency.network.derivationPath,
        ).getOrElse { throwable ->
            showErrorDialog(throwable)
            return@resource
        }
        if (!isDerivationSupported) {
            showDerivationPathNotSupportedDialog()
            return@resource
        }

        useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { throwable ->
            showErrorDialog(throwable)
            return@resource
        }

        val event = CustomTokenAnalyticsEvent.CustomTokenWasAdded(
            currencySymbol = currency.symbol,
            derivationPath = currency.network.derivationPath.value.orEmpty(),
            source = params.source,
        )
        analyticsEventHandler.send(event)

        params.onCurrencyAdded(currency)
    }

    private fun selectNetwork() {
        params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm))
    }

    private fun selectDerivationPath() {
        params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm))
    }

    private fun sendFieldAnalyticsEvent(field: Field, fieldValue: TextInputFieldUM) {
        val event = when (field) {
            Field.CONTRACT_ADDRESS -> CustomTokenAnalyticsEvent.Address(
                isValid = fieldValue.error == null,
                source = params.source,
            )
            Field.NAME -> CustomTokenAnalyticsEvent.Name(params.source)
            Field.SYMBOL -> CustomTokenAnalyticsEvent.Symbol(params.source)
            Field.DECIMALS -> CustomTokenAnalyticsEvent.Decimals(params.source)
        }

        analyticsEventHandler.send(event)
    }
}