package com.tangem.features.addressbook.list.model

import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
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.navigation.Router
import com.tangem.domain.addressbook.interactor.GetVerifiedContactsInteractor
import com.tangem.domain.addressbook.model.VerifiedContact
import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.addressbook.ContactSelectionTrigger
import com.tangem.features.addressbook.MatchedContact
import com.tangem.features.addressbook.SelectedContact
import com.tangem.features.addressbook.analytics.AddressBookEvents.ContactListScreenOpened.Source
import com.tangem.features.addressbook.common.AddressBookAnalyticsSender
import com.tangem.features.addressbook.list.DefaultAddressBookListComponent
import com.tangem.features.addressbook.list.state.AddressBookListStateController
import com.tangem.features.addressbook.list.state.transformers.UpdateAddressBookListContentTransformer
import com.tangem.features.addressbook.list.state.transformers.UpdateAddressBookListQueryTransformer
import com.tangem.features.addressbook.list.ui.state.AddressBookListUM
import com.tangem.features.addressbook.route.AddressBookRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
 * Backs the contacts list. The list content is the same however the address book was opened — the open
 * [AddressBookRoute.ListMode] only decides what tapping a contact does:
 *  - [AddressBookRoute.ListMode.Default]: browse / manage contacts
 *  - [AddressBookRoute.ListMode.Selector]: pick a recipient for the given network — a single matching address is
 *    returned right away, several open the address selector first.
 */
@Suppress("LongParameterList", "NamedArguments")
@OptIn(ExperimentalCoroutinesApi::class)
@ModelScoped
internal class AddressBookListModel @Inject constructor(
    paramsContainer: ParamsContainer,
    override val dispatchers: CoroutineDispatcherProvider,
    private val stateController: AddressBookListStateController,
    private val router: Router,
    private val contactSelectionTrigger: ContactSelectionTrigger,
    private val analyticsSender: AddressBookAnalyticsSender,
    private val syncAddressBooksUseCase: SyncAddressBooksUseCase,
    private val getVerifiedContactsInteractor: GetVerifiedContactsInteractor,
    private val getWalletsUseCase: GetWalletsUseCase,
) : Model() {

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

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

    /** Address-selector bottom sheet, shown when a picked contact has more than one address in the target network. */
    val selectorNavigation = SlotNavigation<MatchedContact>()

    private val searchQuery = MutableStateFlow(value = "")
    private val searchActive = MutableStateFlow(value = false)
    private val selectedWalletId = MutableStateFlow<String?>(value = null)

    private val allContacts: SharedFlow<List<VerifiedContact>> =
        getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null)
            .shareIn(modelScope, SharingStarted.Lazily, replay = 1)

    init {
        modelScope.launch {
            syncAddressBooksUseCase()
            sendContactListScreenOpenedEvent()
            observeContacts()
        }
    }

    private suspend fun observeContacts() {
        val matchedContacts = searchQuery.flatMapLatest { query ->
            if (query.isBlank()) {
                allContacts
            } else {
                getVerifiedContactsInteractor.getVerifiedContacts(query = query, userWalletId = null)
            }
        }
        combine(
            allContacts,
            matchedContacts,
            searchQuery,
            selectedWalletId,
            getWalletsUseCase.invokeAsMap(isOnlyMultiCurrency = false, filterLocked = true),
        ) { all, matched, query, selected, wallets ->
            ListInputs(
                allContacts = all,
                matchedContacts = matched,
                query = query,
                selectedWalletId = selected,
                wallets = wallets,
            )
        }
            .onEach(::updateState)
            .flowOn(dispatchers.default)
            .collect()
    }

    fun deliverSelection(contact: SelectedContact) {
        contactSelectionTrigger.trigger(contact)
        selectorNavigation.dismiss()
        router.pop()
    }

    private fun updateState(inputs: ListInputs) {
        stateController.update(
            UpdateAddressBookListContentTransformer(
                allContacts = inputs.allContacts,
                matchedContacts = inputs.matchedContacts,
                mode = params.mode,
                wallets = inputs.wallets,
                selectedWalletId = inputs.selectedWalletId,
                query = inputs.query,
                onContactClick = params.onContactClick,
                onPickContact = ::onPickContact,
                onQueryChange = ::onQueryChange,
                onActiveChange = ::onActiveChange,
                onClearQuery = ::onClearQuery,
                onChipSelected = ::onChipSelected,
                onAddContactClick = params.onAddContactClick,
            ),
        )
    }

    private fun onQueryChange(query: String) {
        searchQuery.value = query
        updateSearchBar(query = query, isActive = searchActive.value)
    }

    private fun onActiveChange(active: Boolean) {
        searchActive.value = active
        updateSearchBar(query = searchQuery.value, isActive = active)
    }

    private fun onClearQuery() {
        searchQuery.value = ""
        updateSearchBar(query = "", isActive = searchActive.value)
    }

    private fun updateSearchBar(query: String, isActive: Boolean) {
        stateController.update(UpdateAddressBookListQueryTransformer(query = query, isActive = isActive))
    }

    private fun onChipSelected(walletId: String?) {
        if (selectedWalletId.value == walletId) return
        selectedWalletId.value = walletId
    }

    private fun onPickContact(contact: MatchedContact) {
        // Reported on tap, before the address is substituted; onPickContact is only invoked in selector (Send) mode.
        analyticsSender.sendContactSelectedInSend(contactId = contact.contactId, scope = modelScope)
        val singleEntry = contact.entries.singleOrNull()
        if (singleEntry != null) {
            deliverSelection(contact.toSelectedContact(singleEntry))
        } else {
            selectorNavigation.activate(contact)
        }
    }

    private suspend fun sendContactListScreenOpenedEvent() {
        val contacts = allContacts.first()
        analyticsSender.sendContactListScreenOpened(
            source = params.mode.toAnalyticsSource(),
            contactsCount = contacts.size,
            scope = modelScope,
        )
    }

    private fun AddressBookRoute.ListMode.toAnalyticsSource(): Source = when (this) {
        AddressBookRoute.ListMode.Default -> Source.Settings
        is AddressBookRoute.ListMode.Selector -> Source.SendFlow
    }

    private data class ListInputs(
        val allContacts: List<VerifiedContact>,
        val matchedContacts: List<VerifiedContact>,
        val query: String,
        val selectedWalletId: String?,
        val wallets: Map<UserWalletId, UserWallet>,
    )
}