package com.github.nullptroma.wallenc.ui.screens.sync

import androidx.lifecycle.viewModelScope
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.ViewModelBase
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
import com.github.nullptroma.wallenc.ui.resources.resolve
import com.github.nullptroma.wallenc.ui.resources.UserNotification
import com.github.nullptroma.wallenc.usecases.AddStorageToSyncGroupResult
import com.github.nullptroma.wallenc.usecases.ManageStorageSyncGroupsUseCase
import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
import com.github.nullptroma.wallenc.usecases.StorageSyncCompatibilityInput
import com.github.nullptroma.wallenc.usecases.isStorageCompatibleWithGroup
import com.github.nullptroma.wallenc.vault.contract.DescribedVault
import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject

@HiltViewModel
@OptIn(ExperimentalCoroutinesApi::class)
class StorageSyncViewModel @Inject constructor(
    private val groupsUseCase: ManageStorageSyncGroupsUseCase,
    private val runStorageSyncUseCase: RunStorageSyncUseCase,
    private val vaultsManager: IVaultsManager,
    private val taskOrchestrator: ITaskOrchestrator,
    private val uiStrings: UiStringResolver,
) : ViewModelBase<StorageSyncScreenState>(StorageSyncScreenState()) {
    private var storageByUuid: Map<UUID, IStorage> = emptyMap()

    init {
        refreshGroups()
        observeVaults()
        observeStorageSyncPipeline()
        viewModelScope.launch {
            combine(
                vaultsManager.vaults,
                state.map { it.groups },
            ) { vaults, groups ->
                val requiredUuids = groups.flatMap { it.storageUuids }.toSet()
                if (requiredUuids.isEmpty() || vaults.isEmpty()) {
                    false
                } else {
                    val opened = vaultsManager.unlockManager.openedStorages.value
                    vaults.any { vault ->
                        val uuidsInVault = vault.storages.value.flatMap { root ->
                            flattenStorages(buildStorageTree(root, opened))
                        }.map { it.uuid }.toSet()
                        uuidsInVault.any { it in requiredUuids } &&
                            vault.storagesScanInProgress.value
                    }
                }
            }
                .distinctUntilChanged()
                .collect { scanning ->
                    updateState(state.value.copy(anyVaultStoragesScanning = scanning))
                }
        }
    }

    private fun observeStorageSyncPipeline() {
        viewModelScope.launch {
            combine(
                runStorageSyncUseCase.syncRunning,
                runStorageSyncUseCase.activeSyncTaskId,
                taskOrchestrator.pipelineState,
            ) { syncRunning, taskId, pipe ->
                val progress = taskId?.let { id ->
                    val task = pipe.tasks.find { it.id == id }
                    (task?.state as? TaskRunState.Running)?.progress
                }
                Triple(
                    syncRunning,
                    progress?.fraction,
                    progress?.label?.resolve(uiStrings),
                )
            }
                .distinctUntilChanged()
                .collect { (running, frac, label) ->
                    updateState(
                        state.value.copy(
                            isStorageSyncRunning = running,
                            storageSyncProgressFraction = if (running) frac else null,
                            storageSyncProgressLabel = if (running) label else null,
                        ),
                    )
                }
        }
    }

    fun refreshGroups() {
        viewModelScope.launch {
            updateState(state.value.copy(groups = reloadGroupsUi()))
        }
    }

    fun createGroup() {
        viewModelScope.launch {
            withGroupMutationBusy(clearPicker = true) {
                val group = groupsUseCase.createGroup()
                UserNotification.TextRes(
                    R.string.sync_msg_group_created,
                    listOf(group.id),
                )
            }
        }
    }

    fun removeGroup(groupId: String) {
        viewModelScope.launch {
            withGroupMutationBusy {
                groupsUseCase.removeGroup(groupId)
                UserNotification.TextRes(R.string.sync_msg_group_removed)
            }
        }
    }

    fun openPicker(groupId: String) {
        if (state.value.isBusy || state.value.isStorageSyncRunning) return
        updateState(
            state.value.copy(
                pickerGroupId = groupId,
                userMessage = null,
            ),
        )
    }

    fun closePicker() {
        updateState(
            state.value.copy(
                pickerGroupId = null,
                userMessage = null,
            ),
        )
    }

    fun toggleVaultExpanded(vaultUuid: UUID) {
        val expanded = state.value.expandedVaultUuids.toMutableSet()
        if (!expanded.add(vaultUuid)) {
            expanded.remove(vaultUuid)
        }
        updateState(state.value.copy(expandedVaultUuids = expanded))
    }

    fun addStorageToCurrentGroup(storageUuid: UUID) {
        val groupId = state.value.pickerGroupId ?: return
        viewModelScope.launch {
            withGroupMutationBusy {
                val storage = storageByUuid[storageUuid]
                    ?: return@withGroupMutationBusy UserNotification.TextRes(R.string.sync_storage_not_in_vaults)
                val isEncrypted = storage.metaInfo.value.encInfo != null
                val result = groupsUseCase.addStorageToGroup(
                    groupId = groupId,
                    storageUuid = storageUuid,
                    compatibility = StorageSyncCompatibilityInput(
                        isEncrypted = isEncrypted,
                        encryptionSecret = null,
                    ),
                )
                when (result) {
                    AddStorageToSyncGroupResult.Added -> UserNotification.TextRes(
                        R.string.sync_msg_storage_added,
                        listOf(groupId),
                    )
                    AddStorageToSyncGroupResult.AlreadyInGroup -> UserNotification.TextRes(
                        R.string.sync_msg_storage_already_added,
                    )
                    AddStorageToSyncGroupResult.EncryptedStorageNotAllowed -> UserNotification.TextRes(
                        R.string.sync_msg_only_plain_storage_allowed,
                    )
                    AddStorageToSyncGroupResult.MissingEncryptionSecret -> UserNotification.TextRes(
                        R.string.sync_msg_storage_encryption_key_required,
                    )
                    AddStorageToSyncGroupResult.IncompatibleEncryption -> UserNotification.TextRes(
                        R.string.sync_msg_storage_incompatible_encryption,
                    )
                    AddStorageToSyncGroupResult.GroupNotFound -> UserNotification.TextRes(
                        R.string.sync_msg_group_removed,
                    )
                }
            }
        }
    }

    fun removeStorageFromGroup(groupId: String, storageUuid: UUID) {
        viewModelScope.launch {
            withGroupMutationBusy {
                groupsUseCase.removeStorageFromGroup(groupId, storageUuid)
                UserNotification.TextRes(
                    R.string.sync_msg_storage_removed,
                    listOf(groupId),
                )
            }
        }
    }

    fun runSyncNow() {
        val started = runStorageSyncUseCase.enqueue(StorageSyncTriggerReason.SyncTab)
        if (!started) {
            updateState(
                state.value.copy(
                    userMessage = UserNotification.TextRes(R.string.sync_msg_sync_already_running),
                ),
            )
        }
    }

    fun consumeUserMessage() {
        updateState(state.value.copy(userMessage = null))
    }

    private fun observeVaults() {
        viewModelScope.launch {
            vaultsManager.vaults
                .flatMapLatest { vaults ->
                    if (vaults.isEmpty()) {
                        flowOf(emptyList())
                    } else {
                        combine(vaults.map { it.storages }) { rootStoragesByVault ->
                            vaults.zip(rootStoragesByVault.toList())
                        }.flatMapLatest { vaultWithRoots ->
                            vaultsManager.unlockManager.openedStorages.flatMapLatest { opened ->
                                val vaultNodes = vaultWithRoots.map { (vault, roots) ->
                                    vault to roots.map { root ->
                                        buildStorageTree(
                                            root = root,
                                            opened = opened,
                                        )
                                    }
                                }
                                val allStorages = vaultNodes
                                    .flatMap { (_, trees) -> trees.flatMap(::flattenStorages) }
                                    .distinctBy { it.uuid }
                                storageByUuid = allStorages.associateBy { it.uuid }

                                if (allStorages.isEmpty()) {
                                    flowOf(
                                        vaultNodes.map { (vault, trees) ->
                                            StorageSyncVaultUi(
                                                uuid = vault.uuid,
                                                title = vaultTitle(vault as? DescribedVault),
                                                type = vaultType(vault as? DescribedVault),
                                                storages = trees.map { tree ->
                                                    toStorageUi(tree)
                                                },
                                            )
                                        },
                                    )
                                } else {
                                    combine(
                                        allStorages.map { storage ->
                                            combine(storage.metaInfo, storage.isAvailable) { meta, avail ->
                                                storage.uuid to StorageSnapshot(meta, avail)
                                            }
                                        },
                                    ) { pairs ->
                                        val snapByUuid = pairs.associate { it.first to it.second }
                                        vaultNodes.map { (vault, trees) ->
                                            StorageSyncVaultUi(
                                                uuid = vault.uuid,
                                                title = vaultTitle(vault as? DescribedVault),
                                                type = vaultType(vault as? DescribedVault),
                                                storages = trees.map { tree ->
                                                    toStorageUi(tree, snapByUuid)
                                                },
                                            )
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                .collect { mapped ->
                    updateState(
                        state.value.copy(
                            vaults = mapped,
                            groups = reloadGroupsUi(),
                        ),
                    )
                }
        }
    }

    private fun vaultType(vault: DescribedVault?): String {
        return when (val descriptor = vault?.descriptor) {
            is VaultDescriptor.LocalDevice -> uiStrings(R.string.vault_type_local_device)
            is VaultDescriptor.LinkedRemote -> uiStrings(R.string.vault_type_remote, descriptor.brand.name)
            null -> uiStrings(R.string.vault_type_unknown)
        }
    }

    private fun vaultTitle(vault: DescribedVault?): String {
        return when (val descriptor = vault?.descriptor) {
            is VaultDescriptor.LocalDevice -> uiStrings(R.string.vault_title_local)
            is VaultDescriptor.LinkedRemote -> descriptor.accountDisplayName
            null -> uiStrings(R.string.vault_title_unknown)
        }
    }

    private fun buildStorageTree(
        root: IStorage,
        opened: Map<UUID, IStorage>,
        visited: MutableSet<UUID> = mutableSetOf(),
    ): StorageTreeNode {
        if (!visited.add(root.uuid)) {
            return StorageTreeNode(storage = root, children = emptyList())
        }
        val child = opened[root.uuid]
        val children = if (child == null) {
            emptyList()
        } else {
            listOf(
                buildStorageTree(
                    root = child,
                    opened = opened,
                    visited = visited,
                ),
            )
        }
        return StorageTreeNode(storage = root, children = children)
    }

    private fun flattenStorages(node: StorageTreeNode): List<IStorage> {
        return listOf(node.storage) + node.children.flatMap(::flattenStorages)
    }

    private fun toStorageUi(
        node: StorageTreeNode,
        snapshotByUuid: Map<UUID, StorageSnapshot> = emptyMap(),
    ): StorageSyncStorageUi {
        val snap = snapshotByUuid[node.storage.uuid]
        val meta = snap?.meta ?: node.storage.metaInfo.value
        val isReachable = snap?.isAvailable ?: node.storage.isAvailable.value
        val encryptionKind = when {
            meta.encInfo == null -> StorageSyncEncryptionKind.NotEncrypted
            node.storage.isVirtualStorage -> StorageSyncEncryptionKind.EncryptedOpened
            else -> StorageSyncEncryptionKind.EncryptedClosed
        }
        return StorageSyncStorageUi(
            uuid = node.storage.uuid,
            name = meta.name ?: uiStrings(R.string.no_name),
            encryptionKind = encryptionKind,
            isReachable = isReachable,
            children = node.children.map { child ->
                toStorageUi(
                    node = child,
                    snapshotByUuid = snapshotByUuid,
                )
            },
        )
    }

    private data class StorageSnapshot(
        val meta: IStorageMetaInfo,
        val isAvailable: Boolean,
    )

    private data class StorageTreeNode(
        val storage: IStorage,
        val children: List<StorageTreeNode>,
    )

    private suspend fun reloadGroupsUi(): List<StorageSyncGroupUi> =
        groupsUseCase.getGroups().map { group ->
            val incompatible = group.storageUuids.filterTo(mutableSetOf()) { uuid ->
                val storage = storageByUuid[uuid] ?: return@filterTo true
                !isStorageCompatibleWithGroup(
                    storage = storage,
                    group = group,
                )
            }
            StorageSyncGroupUi(
                id = group.id,
                storageUuids = group.storageUuids,
                encryptionKind = group.encryptionKind,
                incompatibleStorageUuids = incompatible,
            )
        }

    private suspend fun withGroupMutationBusy(
        clearPicker: Boolean = false,
        block: suspend () -> UserNotification?,
    ) {
        if (state.value.isStorageSyncRunning) {
            updateState(
                state.value.copy(
                    userMessage = UserNotification.TextRes(R.string.sync_msg_blocked_during_sync),
                ),
            )
            return
        }
        updateState(state.value.copy(isBusy = true, userMessage = null))
        val message = block()
        val groups = reloadGroupsUi()
        updateState(
            state.value.copy(
                groups = groups,
                pickerGroupId = if (clearPicker) null else state.value.pickerGroupId,
                isBusy = false,
                userMessage = message,
            ),
        )
    }
}
