package com.github.nullptroma.wallenc.domain.vault.vaults.yandex

import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskAuthException
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.repository.YandexDiskRepository
import com.github.nullptroma.wallenc.domain.vault.storages.yandex.YandexStorage
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
import com.github.nullptroma.wallenc.vault.contract.DescribedVault
import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.UUID

/**
 * Удалённый vault Яндекс.Диска: папка приложения `app:/`, внутри — подпапки-UUID как [YandexStorage].
 *
 * [isAvailable] — успешный контакт с Disk API (метаданные диска и загрузка списка storages).
 * Пока false, дочерние storages тоже считаются недоступными (см. YandexStorageAccessor).
 */
class YandexVault(
    override val uuid: UUID,
    accountEmail: String,
    private val repo: YandexDiskRepository,
    private val ioDispatcher: CoroutineDispatcher,
    private val parentScope: CoroutineScope,
) : DescribedVault {

    override val descriptor: VaultDescriptor = VaultDescriptor.LinkedRemote(
        uuid = uuid,
        brand = CloudBrand.YANDEX,
        accountDisplayName = accountEmail,
    )

    private val _vaultReachable = MutableStateFlow(false)
    override val isAvailable: StateFlow<Boolean> = _vaultReachable

    private val _storages = MutableStateFlow<List<IStorage>>(emptyList())
    override val storages: StateFlow<List<IStorage>> = _storages

    private val _storagesScanInProgress = MutableStateFlow(false)
    override val storagesScanInProgress: StateFlow<Boolean> = _storagesScanInProgress

    private val _totalSpace = MutableStateFlow<Long?>(null)
    override val totalSpace: StateFlow<Long?> = _totalSpace

    private val _availableSpace = MutableStateFlow<Long?>(null)
    override val availableSpace: StateFlow<Long?> = _availableSpace

    private val refreshMutex = Mutex()

    init {
        parentScope.launch {
            runCatching { refreshFromDisk() }
        }
    }

    override suspend fun rescanStorages() {
        refreshMutex.withLock {
            refreshFromDisk()
        }
    }

    private suspend fun refreshFromDisk() {
        _storagesScanInProgress.value = true
        _vaultReachable.value = false
        try {
            val info = repo.diskInfo()
            _totalSpace.value = info.totalSpace
            val used = info.usedSpace ?: 0L
            val total = info.totalSpace ?: 0L
            _availableSpace.value = (total - used).coerceAtLeast(0L)
            _vaultReachable.value = true
            _storages.value = loadStoragesList()
        } catch (_: YandexDiskAuthException) {
            _vaultReachable.value = false
            _storages.value = emptyList()
        } catch (_: Exception) {
            _vaultReachable.value = false
            _storages.value = emptyList()
        } finally {
            _storagesScanInProgress.value = false
        }
    }

    private suspend fun loadStoragesList(): List<IStorage> {
        val pending = mutableListOf<YandexStorage>()
        var offset = 0
        while (true) {
            val root = repo.list("app:/", APP_LIST_LIMIT, offset)
            val items = root.embedded?.items.orEmpty()
            for (item in items) {
                if (item.type != "dir") continue
                val name = item.name ?: continue
                val storageUuid = runCatching { UUID.fromString(name) }.getOrNull() ?: continue
                pending.add(
                    YandexStorage(
                        uuid = storageUuid,
                        repo = repo,
                        vaultAvailability = _vaultReachable,
                        ioDispatcher = ioDispatcher,
                        accessorScope = parentScope,
                        reportAuthFailure = {
                            parentScope.launch { _vaultReachable.value = false }
                        },
                    ),
                )
            }
            if (items.size < APP_LIST_LIMIT) break
            offset += items.size
        }
        if (pending.isEmpty()) return emptyList()
        return coroutineScope {
            pending.map { storage ->
                async(ioDispatcher) { initStorageWithRetry(storage) }
            }.awaitAll().filterNotNull()
        }
    }

    private suspend fun initStorageWithRetry(storage: YandexStorage): YandexStorage? {
        for (attempt in 0 until STORAGE_INIT_ATTEMPTS) {
            if (attempt > 0) {
                delay(STORAGE_INIT_RETRY_DELAY_MS * attempt)
            }
            if (
                runCatching { storage.init() }.isSuccess &&
                storage.metaLoadState.value == StorageMetaLoadState.Ready
            ) {
                return storage
            }
        }
        return null
    }

    override suspend fun createStorage(): IStorage = withContext(ioDispatcher) {
        val id = UUID.randomUUID()
        repo.createFolder("app:/$id")
        val storage = YandexStorage(
            uuid = id,
            repo = repo,
            vaultAvailability = _vaultReachable,
            ioDispatcher = ioDispatcher,
            accessorScope = parentScope,
            reportAuthFailure = {
                parentScope.launch { _vaultReachable.value = false }
            },
        )
        storage.init()
        _storages.value += storage
        storage
    }

    override suspend fun createStorage(enc: StorageEncryptionInfo): IStorage {
        val storage = createStorage()
        storage.setEncInfo(enc)
        return storage
    }

    override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
        if (storage !is YandexStorage) return@withContext
        repo.delete("app:/${storage.uuid}", permanently = true)
        _storages.value = _storages.value.filter { it.uuid != storage.uuid }
    }

    private companion object {
        private const val APP_LIST_LIMIT = 1000
        private const val STORAGE_INIT_ATTEMPTS = 3
        private const val STORAGE_INIT_RETRY_DELAY_MS = 400L
    }
}
