package ru.forbion.f7cloud.feature.mail

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
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.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import ru.forbion.f7cloud.core.auth.AuthSession
import ru.forbion.f7cloud.core.network.UnauthorizedException
import java.util.concurrent.ConcurrentHashMap

data class MailUiState(
    val loading: Boolean = false,
    val loadingMore: Boolean = false,
    val accounts: List<MailAccount> = emptyList(),
    val mailboxes: List<MailMailbox> = emptyList(),
    val folders: List<MailFolderEntry> = emptyList(),
    val selectedFolder: MailFolderEntry? = null,
    val messages: List<MailMessage> = emptyList(),
    val nextCursor: Long? = null,
    val searchQuery: String = "",
    val searchParams: MailSearchParams = MailSearchParams(),
    val collapsedAccountIds: Set<Int> = emptySet(),
    val selectedMessageId: Int? = null,
    val messageDetail: MailMessageDetail? = null,
    val messageLoading: Boolean = false,
    val threadMessages: List<MailMessage> = emptyList(),
    val threadExpanded: Boolean = false,
    val outboxMessages: List<MailOutboxMessage> = emptyList(),
    val editingAccountId: Int? = null,
    val accountSettings: MailAccountSettings? = null,
    val accountSettingsLoading: Boolean = false,
    val accountSettingsSaving: Boolean = false,
    val appSettings: MailAppSettings? = null,
    val appSettingsLoading: Boolean = false,
    val appSettingsSaving: Boolean = false,
    val error: String? = null,
    val unauthorized: Boolean = false,
    // «Создать задачу/событие из письма» (паритет живого веба)
    val createTaskSheetOpen: Boolean = false,
    val createEventSheetOpen: Boolean = false,
    val createTargets: List<MailTargetList> = emptyList(),
    val creatingFromMessage: Boolean = false,
    // Фильтры почты (официальный API NC Mail — общие с вебом)
    val sieveAccountId: Int? = null,
    val sieveRules: List<MailFilterRule> = emptyList(),
    val sieveLoading: Boolean = false,
    val sieveSaving: Boolean = false,
    val sieveError: String? = null,
)

class MailViewModel(
    private val repository: MailRepository = MailRepository(),
    private val cacheRepository: MailCacheRepository? = null,
) : ViewModel() {
    private val _state = MutableStateFlow(MailUiState())
    val state: StateFlow<MailUiState> = _state.asStateFlow()
    private var markReadJob: Job? = null
    private var prefetchJob: Job? = null
    private val prefetchInFlight = ConcurrentHashMap.newKeySet<Int>()
    private val folderCache = mutableMapOf<String, MailMessagesPage>()
    private var syncJob: Job? = null

    fun load(session: AuthSession, forceRefresh: Boolean = false) {
        syncJob?.cancel()
        if (forceRefresh) {
            folderCache.clear()
        }
        viewModelScope.launch(Dispatchers.IO) {
            val cachedBootstrap = cacheRepository?.loadBootstrap(session)
            if (cachedBootstrap != null) {
                restoreFromBootstrap(session, cachedBootstrap)
            }
            val folder = _state.value.selectedFolder
            if (folder?.specialRole == "outbox") {
                if (forceRefresh || _state.value.outboxMessages.isEmpty()) {
                    fetchOutboxMessages(session)
                }
                return@launch
            }
            if (folder != null && folder.specialRole != "outbox") {
                val cachedPage = folderCache[folder.cacheKey()]
                    ?: cacheRepository?.loadFolderPage(session, folder)
                if (cachedPage != null) {
                    folderCache[folder.cacheKey()] = cachedPage
                    _state.value = _state.value.copy(
                        messages = cachedPage.messages,
                        nextCursor = normalizePage(cachedPage).nextCursor,
                        loading = false,
                    )
                }
            }
            if (!forceRefresh && folder != null && !isFolderStale(session, folder)) {
                scheduleMessagePrefetch(session, folder, _state.value.messages)
                return@launch
            }
            syncBootstrapAndFolder(session, forceRefresh)
        }
    }

    fun selectFolder(session: AuthSession, folder: MailFolderEntry) {
        val cached = folderCache[folder.cacheKey()]
        _state.value = _state.value.copy(
            selectedFolder = folder,
            selectedMessageId = null,
            messageDetail = null,
            searchQuery = "",
            searchParams = MailSearchParams(),
            messages = cached?.messages.orEmpty(),
            outboxMessages = emptyList(),
            nextCursor = cached?.let { normalizePage(it).nextCursor },
            loading = cached == null,
            error = null,
        )
        if (cached == null) {
            viewModelScope.launch(Dispatchers.IO) {
                cacheRepository?.loadFolderPage(session, folder)?.let { page ->
                    folderCache[folder.cacheKey()] = page
                    if (_state.value.selectedFolder?.cacheKey() == folder.cacheKey()) {
                        _state.value = _state.value.copy(
                            messages = page.messages,
                            nextCursor = normalizePage(page).nextCursor,
                            loading = false,
                        )
                    }
                    scheduleMessagePrefetch(session, folder, page.messages)
                }
                if (folderCache[folder.cacheKey()] == null) {
                    fetchFolderMessages(session, folder, forceRefresh = false)
                } else if (isFolderStale(session, folder)) {
                    fetchFolderMessages(session, folder, forceRefresh = true)
                }
            }
        } else {
            scheduleMessagePrefetch(session, folder, cached.messages)
            viewModelScope.launch(Dispatchers.IO) {
                if (isFolderStale(session, folder)) {
                    fetchFolderMessages(session, folder, forceRefresh = true)
                }
            }
        }
    }

    fun setSearchQuery(session: AuthSession, query: String) {
        _state.value = _state.value.copy(searchQuery = query)
        val folder = _state.value.selectedFolder ?: return
        fetchFolderMessages(session, folder, forceRefresh = true)
    }

    fun applySearchParams(session: AuthSession, params: MailSearchParams) {
        _state.value = _state.value.copy(searchParams = params)
        val folder = _state.value.selectedFolder ?: return
        fetchFolderMessages(session, folder, forceRefresh = true)
    }

    fun clearSearchParams(session: AuthSession) {
        _state.value = _state.value.copy(searchParams = MailSearchParams())
        val folder = _state.value.selectedFolder ?: return
        fetchFolderMessages(session, folder, forceRefresh = true)
    }

    fun toggleQuickFilter(session: AuthSession, filter: MailQuickFilter) {
        applySearchParams(session, _state.value.searchParams.toggleQuickFilter(filter))
    }

    fun toggleThreadExpanded() {
        _state.value = _state.value.copy(threadExpanded = !_state.value.threadExpanded)
    }

    fun selectOutbox(session: AuthSession) {
        _state.value = _state.value.copy(
            selectedFolder = MailVirtualFolders.OUTBOX,
            selectedMessageId = null,
            messageDetail = null,
            searchQuery = "",
            searchParams = MailSearchParams(),
            messages = emptyList(),
            outboxMessages = emptyList(),
            nextCursor = null,
            loading = true,
            error = null,
        )
        fetchOutboxMessages(session)
    }

    fun retryOutboxMessage(session: AuthSession, messageId: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.sendOutboxMessage(session, messageId) }
                .onSuccess { fetchOutboxMessages(session) }
                .onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun deleteOutboxMessage(session: AuthSession, messageId: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.deleteOutboxMessage(session, messageId) }
                .onSuccess {
                    _state.value = _state.value.copy(
                        outboxMessages = _state.value.outboxMessages.filter { it.id != messageId },
                    )
                }
                .onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun loadAppSettings(session: AuthSession) {
        _state.value = _state.value.copy(appSettingsLoading = true)
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.loadAppSettings(session) }
                .onSuccess { settings ->
                    _state.value = _state.value.copy(
                        appSettings = settings,
                        appSettingsLoading = false,
                    )
                }
                .onFailure { t ->
                    _state.value = _state.value.copy(
                        appSettingsLoading = false,
                        error = t.message,
                    )
                }
        }
    }

    fun setShowThreaded(session: AuthSession, enabled: Boolean) {
        val previous = _state.value.appSettings ?: return
        val value = if (enabled) "threaded" else "singleton"
        _state.value = _state.value.copy(
            appSettings = previous.copy(showThreaded = enabled),
            appSettingsSaving = true,
        )
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.savePreference(session, "layout-message-view", value) }
                .onSuccess {
                    _state.value = _state.value.copy(appSettingsSaving = false)
                }
                .onFailure { t ->
                    _state.value = _state.value.copy(
                        appSettings = previous,
                        appSettingsSaving = false,
                        error = t.message,
                    )
                }
        }
    }

    fun setHighlightExternalAddresses(session: AuthSession, enabled: Boolean) {
        val previous = _state.value.appSettings ?: return
        _state.value = _state.value.copy(
            appSettings = previous.copy(highlightExternalAddresses = enabled),
            appSettingsSaving = true,
        )
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                repository.savePreference(session, "internal-addresses", if (enabled) "true" else "false")
            }.onSuccess {
                _state.value = _state.value.copy(appSettingsSaving = false)
            }.onFailure { t ->
                _state.value = _state.value.copy(
                    appSettings = previous,
                    appSettingsSaving = false,
                    error = t.message,
                )
            }
        }
    }

    fun removeTrustedSender(session: AuthSession, sender: MailTrustedSender) {
        val previous = _state.value.appSettings ?: return
        _state.value = _state.value.copy(
            appSettings = previous.copy(
                trustedSenders = previous.trustedSenders.filter { it.id != sender.id },
            ),
        )
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.removeTrustedSender(session, sender.email, sender.type) }
                .onFailure { t ->
                    _state.value = _state.value.copy(
                        appSettings = previous,
                        error = t.message,
                    )
                }
        }
    }

    fun createTextBlock(session: AuthSession, title: String, content: String) {
        viewModelScope.launch(Dispatchers.IO) {
            _state.value = _state.value.copy(appSettingsSaving = true)
            runCatching { repository.createTextBlock(session, title, content) }
                .onSuccess { block ->
                    val current = _state.value.appSettings
                    if (current != null) {
                        _state.value = _state.value.copy(
                            appSettings = current.copy(
                                textBlocks = (current.textBlocks + block).sortedBy { it.title.lowercase() },
                            ),
                            appSettingsSaving = false,
                        )
                    } else {
                        _state.value = _state.value.copy(appSettingsSaving = false)
                    }
                }
                .onFailure { t ->
                    _state.value = _state.value.copy(
                        appSettingsSaving = false,
                        error = t.message,
                    )
                }
        }
    }

    fun deleteTextBlock(session: AuthSession, blockId: Int) {
        val previous = _state.value.appSettings ?: return
        _state.value = _state.value.copy(
            appSettings = previous.copy(
                textBlocks = previous.textBlocks.filter { it.id != blockId },
            ),
        )
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.deleteTextBlock(session, blockId) }
                .onFailure { t ->
                    _state.value = _state.value.copy(
                        appSettings = previous,
                        error = t.message,
                    )
                }
        }
    }

    fun openAccountSettings(session: AuthSession, accountId: Int) {
        _state.value = _state.value.copy(
            editingAccountId = accountId,
            accountSettings = null,
            accountSettingsLoading = true,
        )
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.loadAccountSettings(session, accountId) }
                .onSuccess { settings ->
                    if (_state.value.editingAccountId == accountId) {
                        _state.value = _state.value.copy(
                            accountSettings = settings,
                            accountSettingsLoading = false,
                        )
                    }
                }
                .onFailure { t ->
                    _state.value = _state.value.copy(
                        accountSettingsLoading = false,
                        editingAccountId = null,
                        error = t.message,
                    )
                }
        }
    }

    fun closeAccountSettings() {
        _state.value = _state.value.copy(
            editingAccountId = null,
            accountSettings = null,
            accountSettingsLoading = false,
            accountSettingsSaving = false,
        )
    }

    fun saveAccountSettings(session: AuthSession, settings: MailAccountSettings) {
        viewModelScope.launch(Dispatchers.IO) {
            _state.value = _state.value.copy(accountSettingsSaving = true)
            runCatching {
                repository.updateAccountSignature(session, settings.id, settings.signature)
                repository.patchAccountSettings(
                    session = session,
                    accountId = settings.id,
                    draftsMailboxId = settings.draftsMailboxId,
                    sentMailboxId = settings.sentMailboxId,
                    trashMailboxId = settings.trashMailboxId,
                    archiveMailboxId = settings.archiveMailboxId,
                    junkMailboxId = settings.junkMailboxId,
                    searchBody = settings.searchBody,
                    classificationEnabled = settings.classificationEnabled,
                    signatureAboveQuote = settings.signatureAboveQuote,
                    imipCreate = settings.imipCreate,
                )
            }.onSuccess { updated ->
                _state.value = _state.value.copy(
                    accountSettings = updated,
                    accountSettingsSaving = false,
                    editingAccountId = null,
                )
                load(session, forceRefresh = true)
            }.onFailure { t ->
                _state.value = _state.value.copy(
                    accountSettingsSaving = false,
                    error = t.message,
                )
            }
        }
    }

    fun toggleAccountCollapsed(accountId: Int) {
        val current = _state.value.collapsedAccountIds
        _state.value = _state.value.copy(
            collapsedAccountIds = if (accountId in current) {
                current - accountId
            } else {
                current + accountId
            },
        )
    }

    fun loadMore(session: AuthSession) {
        val folder = _state.value.selectedFolder ?: return
        if (folder.specialRole == "outbox") return
        val cursor = _state.value.nextCursor ?: return
        if (_state.value.loadingMore || _state.value.searchQuery.isNotBlank() || _state.value.searchParams.isActive()) return
        viewModelScope.launch(Dispatchers.IO) {
            _state.value = _state.value.copy(loadingMore = true)
            runCatching {
                repository.loadMessages(session, folder, cursor = cursor)
            }.onSuccess { page ->
                val merged = (_state.value.messages + page.messages).distinctBy { it.id }
                val mergedPage = MailMessagesPage(merged, page.nextCursor)
                _state.value = _state.value.copy(
                    loadingMore = false,
                    messages = merged,
                    nextCursor = page.nextCursor,
                )
                folderCache[folder.cacheKey()] = mergedPage
                cacheRepository?.saveFolderPage(session, folder, mergedPage)
                scheduleMessagePrefetch(session, folder, page.messages)
            }.onFailure { t ->
                _state.value = _state.value.copy(loadingMore = false, error = t.message)
            }
        }
    }

    fun openDeepLink(session: AuthSession, messageId: Int, mailboxId: Int? = null) {
        viewModelScope.launch(Dispatchers.IO) {
            if (_state.value.folders.isEmpty()) {
                val cachedBootstrap = cacheRepository?.loadBootstrap(session)
                if (cachedBootstrap != null) {
                    restoreFromBootstrap(session, cachedBootstrap)
                } else {
                    runCatching { repository.loadBootstrap(session) }
                        .onSuccess { bootstrap ->
                            applyBootstrap(session, bootstrap)
                        }
                        .onFailure { t ->
                            fail(t)
                            return@launch
                        }
                }
            }
            val folder = mailboxId?.let { id ->
                _state.value.folders.find { it.mailboxId == id }
            } ?: _state.value.selectedFolder ?: _state.value.folders.firstOrNull()
            if (folder != null && folder != _state.value.selectedFolder) {
                selectFolder(session, folder)
            }
            openMessage(session, messageId)
        }
    }

    fun openMessage(session: AuthSession, messageId: Int, forceRefresh: Boolean = false) {
        markReadJob?.cancel()
        val listStub = _state.value.messages.find { it.id == messageId }?.toDetailStub()
            ?: _state.value.threadMessages.find { it.id == messageId }?.toDetailStub()
        _state.value = _state.value.copy(
            selectedMessageId = messageId,
            messageDetail = listStub,
            messageLoading = true,
            threadMessages = emptyList(),
            threadExpanded = false,
            error = null,
        )
        viewModelScope.launch(Dispatchers.IO) {
            val cachedDetail = if (!forceRefresh) cacheRepository?.loadMessageDetail(session, messageId) else null
            if (cachedDetail != null && !forceRefresh) {
                _state.value = _state.value.copy(
                    messageDetail = cachedDetail,
                    messageLoading = false,
                )
                fetchMessageFromNetwork(session, messageId, cachedDetail, backgroundRefresh = true)
            } else {
                fetchMessageFromNetwork(session, messageId, listStub)
            }
        }
    }

    fun closeMessage() {
        markReadJob?.cancel()
        _state.value = _state.value.copy(
            selectedMessageId = null,
            messageDetail = null,
            messageLoading = false,
            threadMessages = emptyList(),
            threadExpanded = false,
        )
    }

    fun toggleStar(session: AuthSession, messageId: Int) {
        val current = findMessageFlags(messageId) ?: return
        val newFlagged = !current.flagged
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                repository.setMessageFlags(session, messageId, flagged = newFlagged)
            }.onSuccess {
                updateMessageFlags(session, messageId, flagged = newFlagged)
                invalidateFilterCaches(session)
                _state.value.messageDetail?.takeIf { it.id == messageId }?.let { detail ->
                    val updated = detail.copy(flags = detail.flags.copy(flagged = newFlagged))
                    _state.value = _state.value.copy(messageDetail = updated)
                    cacheRepository?.saveMessageDetail(session, updated)
                }
            }.onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun toggleRead(session: AuthSession, messageId: Int) {
        val current = findMessageFlags(messageId) ?: return
        val newSeen = !current.seen
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                repository.setMessageFlags(session, messageId, seen = newSeen)
            }.onSuccess {
                updateMessageFlags(session, messageId, seen = newSeen)
                invalidateFilterCaches(session)
                _state.value.messageDetail?.takeIf { it.id == messageId }?.let { detail ->
                    val updated = detail.copy(flags = detail.flags.copy(seen = newSeen))
                    _state.value = _state.value.copy(messageDetail = updated)
                    cacheRepository?.saveMessageDetail(session, updated)
                }
            }.onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun deleteMessage(session: AuthSession, messageId: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.deleteMessage(session, messageId) }
                .onSuccess {
                    removeMessageFromCaches(session, messageId)
                    _state.value = _state.value.copy(
                        messages = _state.value.messages.filter { it.id != messageId },
                        selectedMessageId = null,
                        messageDetail = null,
                    )
                }
                .onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun toggleImportant(session: AuthSession, messageId: Int) {
        val current = findMessageFlags(messageId) ?: return
        val newImportant = !current.important
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                repository.setMessageFlags(session, messageId, important = newImportant)
            }.onSuccess {
                updateMessageFlags(session, messageId, important = newImportant)
                _state.value.messageDetail?.takeIf { it.id == messageId }?.let { detail ->
                    val updated = detail.copy(flags = detail.flags.copy(important = newImportant))
                    _state.value = _state.value.copy(messageDetail = updated)
                    cacheRepository?.saveMessageDetail(session, updated)
                }
            }.onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun markAsSpam(session: AuthSession, messageId: Int, accountId: Int) {
        val junkMailboxId = _state.value.accounts.find { it.id == accountId }?.junkMailboxId ?: return
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.moveMessage(session, messageId, junkMailboxId) }
                .onSuccess {
                    removeMessageFromCaches(session, messageId)
                    _state.value = _state.value.copy(
                        messages = _state.value.messages.filter { it.id != messageId },
                        selectedMessageId = null,
                        messageDetail = null,
                    )
                }
                .onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun moveMessage(session: AuthSession, messageId: Int, destMailboxId: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            runCatching { repository.moveMessage(session, messageId, destMailboxId) }
                .onSuccess {
                    removeMessageFromCaches(session, messageId)
                    _state.value = _state.value.copy(
                        messages = _state.value.messages.filter { it.id != messageId },
                        selectedMessageId = null,
                        messageDetail = null,
                    )
                }
                .onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun snoozeMessage(session: AuthSession, messageId: Int, accountId: Int, sendAtEpochSeconds: Long) {
        val snoozeMailboxId = _state.value.accounts.find { it.id == accountId }?.snoozeMailboxId
            ?: _state.value.mailboxes.firstOrNull { it.accountId == accountId && it.specialRole == "snooze" }?.id
            ?: return
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                repository.snoozeMessage(session, messageId, sendAtEpochSeconds, snoozeMailboxId)
            }.onSuccess {
                removeMessageFromCaches(session, messageId)
                _state.value = _state.value.copy(
                    messages = _state.value.messages.filter { it.id != messageId },
                    selectedMessageId = null,
                    messageDetail = null,
                )
            }.onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun toggleMessageTag(session: AuthSession, messageId: Int, tag: MailTag, add: Boolean) {
        if (tag.imapLabel.isBlank()) return
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                repository.setMessageTag(session, messageId, tag.imapLabel, add)
            }.onSuccess {
                fun updateTags(tags: List<MailTag>): List<MailTag> {
                    val without = tags.filter { it.imapLabel != tag.imapLabel }
                    return if (add) without + tag else without
                }
                _state.value = _state.value.copy(
                    messages = _state.value.messages.map { msg ->
                        if (msg.id != messageId) msg else msg.copy(tags = updateTags(msg.tags))
                    },
                )
                _state.value.messageDetail?.takeIf { it.id == messageId }?.let { detail ->
                    val updated = detail.copy(tags = updateTags(detail.tags))
                    _state.value = _state.value.copy(messageDetail = updated)
                    cacheRepository?.saveMessageDetail(session, updated)
                }
            }.onFailure { t -> _state.value = _state.value.copy(error = t.message) }
        }
    }

    fun downloadAttachment(
        session: AuthSession,
        messageId: Int,
        attachment: MailAttachment,
        onResult: (Result<ByteArray>) -> Unit,
    ) {
        viewModelScope.launch(Dispatchers.IO) {
            onResult(runCatching { repository.downloadAttachment(session, messageId, attachment.id) })
        }
    }

    private suspend fun syncBootstrapAndFolder(session: AuthSession, forceRefresh: Boolean) {
        _state.value = _state.value.copy(
            loading = _state.value.messages.isEmpty() && _state.value.selectedFolder != null,
            error = null,
        )
        runCatching { repository.loadBootstrap(session) }
            .onSuccess { bootstrap ->
                applyBootstrap(session, bootstrap)
                val folder = bootstrap.selectedFolder ?: _state.value.selectedFolder
                if (folder != null) {
                    fetchFolderMessages(session, folder, forceRefresh = forceRefresh)
                } else {
                    _state.value = _state.value.copy(loading = false)
                }
            }
            .onFailure { t ->
                if (_state.value.messages.isEmpty()) {
                    fail(t)
                } else {
                    _state.value = _state.value.copy(loading = false)
                }
            }
    }

    private suspend fun restoreFromBootstrap(session: AuthSession, bootstrap: MailBootstrap) {
        val selected = bootstrap.selectedFolder ?: bootstrap.folders.firstOrNull()
        _state.value = _state.value.copy(
            accounts = bootstrap.accounts,
            mailboxes = bootstrap.mailboxes,
            folders = bootstrap.folders,
            selectedFolder = selected,
            loading = selected != null && _state.value.messages.isEmpty(),
        )
        bootstrap.folders.forEach { folder ->
            if (folderCache.containsKey(folder.cacheKey())) return@forEach
            cacheRepository?.loadFolderPage(session, folder)?.let { page ->
                folderCache[folder.cacheKey()] = page
            }
        }
    }

    private suspend fun applyBootstrap(session: AuthSession, bootstrap: MailBootstrap) {
        val selected = bootstrap.selectedFolder ?: _state.value.selectedFolder ?: bootstrap.folders.firstOrNull()
        _state.value = _state.value.copy(
            accounts = bootstrap.accounts,
            mailboxes = bootstrap.mailboxes,
            folders = bootstrap.folders,
            selectedFolder = selected,
        )
        cacheRepository?.saveBootstrap(
            session,
            bootstrap.copy(selectedFolder = selected),
        )
    }

    private fun fetchFolderMessages(
        session: AuthSession,
        folder: MailFolderEntry,
        forceRefresh: Boolean,
    ) {
        if (folder.specialRole == "outbox") {
            fetchOutboxMessages(session)
            return
        }
        val searchQuery = _state.value.searchQuery
        val searchParams = _state.value.searchParams
        val hasSearch = searchQuery.isNotBlank() || searchParams.isActive()
        syncJob?.cancel()
        syncJob = viewModelScope.launch(Dispatchers.IO) {
            if (!hasSearch && !forceRefresh) {
                val cached = folderCache[folder.cacheKey()]
                    ?: cacheRepository?.loadFolderPage(session, folder)?.also {
                        folderCache[folder.cacheKey()] = it
                    }
                if (cached != null && _state.value.selectedFolder?.cacheKey() == folder.cacheKey()) {
                    _state.value = _state.value.copy(
                        messages = cached.messages,
                        nextCursor = cached.nextCursor,
                        loading = false,
                    )
                    if (!isFolderStale(session, folder)) {
                        scheduleMessagePrefetch(session, folder, cached.messages)
                        return@launch
                    }
                }
            }
            if (!hasSearch && _state.value.messages.isEmpty()) {
                _state.value = _state.value.copy(loading = true, error = null)
            }
            runCatching {
                repository.loadMessages(session, folder, searchQuery, searchParams)
            }.onSuccess { page ->
                _state.value = _state.value.copy(
                    loading = false,
                    messages = page.messages,
                    nextCursor = page.nextCursor,
                )
                if (!hasSearch) {
                    folderCache[folder.cacheKey()] = page
                    cacheRepository?.saveFolderPage(session, folder, page)
                    scheduleMessagePrefetch(session, folder, page.messages)
                }
            }.onFailure { t ->
                if (_state.value.messages.isEmpty()) {
                    fail(t)
                } else {
                    _state.value = _state.value.copy(loading = false)
                }
            }
        }
    }

    private fun fetchOutboxMessages(session: AuthSession) {
        viewModelScope.launch(Dispatchers.IO) {
            _state.value = _state.value.copy(loading = true, error = null)
            runCatching { repository.loadOutboxMessages(session) }
                .onSuccess { messages ->
                    if (_state.value.selectedFolder?.specialRole == "outbox") {
                        _state.value = _state.value.copy(
                            loading = false,
                            outboxMessages = messages,
                            messages = emptyList(),
                            nextCursor = null,
                        )
                    }
                }
                .onFailure { t ->
                    _state.value = _state.value.copy(loading = false, error = t.message)
                }
        }
    }

    private fun scheduleMessagePrefetch(
        session: AuthSession,
        folder: MailFolderEntry,
        messages: List<MailMessage>,
    ) {
        val cache = cacheRepository ?: return
        if (folder.specialRole == "outbox") return
        if (_state.value.searchQuery.isNotBlank() || _state.value.searchParams.isActive()) return
        val folderKey = folder.cacheKey()
        prefetchJob?.cancel()
        prefetchJob = viewModelScope.launch(Dispatchers.IO) {
            val candidates = messages
                .take(MESSAGE_PREFETCH_LIMIT)
                .filter { message ->
                    !cache.isMessageDetailCached(session, message.id) &&
                        !prefetchInFlight.contains(message.id)
                }
            if (candidates.isEmpty()) return@launch
            candidates.chunked(PREFETCH_CONCURRENCY).forEach { batch ->
                if (!isActive) return@launch
                if (_state.value.selectedFolder?.cacheKey() != folderKey) return@launch
                coroutineScope {
                    batch.map { message ->
                        async {
                            if (!isActive) return@async
                            if (_state.value.selectedFolder?.cacheKey() != folderKey) return@async
                            if (!prefetchInFlight.add(message.id)) return@async
                            try {
                                prefetchMessageDetail(session, message)
                            } finally {
                                prefetchInFlight.remove(message.id)
                            }
                        }
                    }.awaitAll()
                }
            }
        }
    }

    private suspend fun prefetchMessageDetail(session: AuthSession, message: MailMessage) {
        val cache = cacheRepository ?: return
        if (cache.isMessageDetailCached(session, message.id)) return
        runCatching {
            val loaded = repository.loadMessage(session, message.id)
            val detail = if (loaded.tags.isEmpty() && message.tags.isNotEmpty()) {
                loaded.copy(tags = message.tags)
            } else {
                loaded
            }
            if (detail.hasBodyContent()) {
                cache.saveMessageDetail(session, detail)
            }
        }
    }

    private suspend fun fetchMessageFromNetwork(
        session: AuthSession,
        messageId: Int,
        fallbackDetail: MailMessageDetail?,
        backgroundRefresh: Boolean = false,
    ) {
        val detailResult = runCatching { repository.loadMessage(session, messageId) }
        detailResult
            .onSuccess { loaded ->
                val listTags = _state.value.messages.find { it.id == messageId }?.tags.orEmpty()
                val detail = if (loaded.tags.isEmpty() && listTags.isNotEmpty()) {
                    loaded.copy(tags = listTags)
                } else {
                    loaded
                }
                if (_state.value.selectedMessageId == messageId) {
                    _state.value = _state.value.copy(
                        messageDetail = detail,
                        messageLoading = false,
                    )
                }
                cacheRepository?.saveMessageDetail(session, detail)
                scheduleMarkRead(session, messageId, detail)
                loadThread(session, messageId)
            }
            .onFailure { t ->
                if (backgroundRefresh && fallbackDetail != null) {
                    return
                }
                if (fallbackDetail != null) {
                    _state.value = _state.value.copy(messageLoading = false)
                } else {
                    _state.value = _state.value.copy(
                        selectedMessageId = null,
                        messageLoading = false,
                        error = t.message,
                        unauthorized = t is UnauthorizedException,
                    )
                }
            }
    }

    private suspend fun loadThread(session: AuthSession, messageId: Int) {
        runCatching { repository.loadThread(session, messageId) }
            .onSuccess { thread ->
                if (_state.value.selectedMessageId == messageId) {
                    _state.value = _state.value.copy(threadMessages = thread)
                }
            }
    }

    private fun scheduleMarkRead(session: AuthSession, messageId: Int, detail: MailMessageDetail) {
        if (detail.flags.seen) return
        markReadJob = viewModelScope.launch(Dispatchers.IO) {
            delay(MARK_READ_DELAY_MS)
            runCatching {
                repository.setMessageFlags(session, messageId, seen = true)
            }.onSuccess {
                updateMessageFlags(session, messageId, seen = true)
                invalidateFilterCaches(session)
            }
        }
    }

    private fun invalidateFilterCaches(session: AuthSession) {
        val filterFolders = _state.value.folders.filter {
            it.filter == MailListFilter.UNREAD || it.filter == MailListFilter.STARRED
        }
        filterFolders.forEach { folderCache.remove(it.cacheKey()) }
        if (filterFolders.isEmpty()) return
        viewModelScope.launch(Dispatchers.IO) {
            coroutineScope {
                filterFolders.map { folder ->
                    async {
                        runCatching { repository.loadMessages(session, folder) }
                            .onSuccess { page ->
                                folderCache[folder.cacheKey()] = page
                                cacheRepository?.saveFolderPage(session, folder, page)
                                applyCacheIfSelected(folder, page)
                                scheduleMessagePrefetch(session, folder, page.messages)
                            }
                    }
                }.awaitAll()
            }
        }
    }

    private fun applyCacheIfSelected(folder: MailFolderEntry, page: MailMessagesPage) {
        val current = _state.value
        if (current.selectedFolder?.cacheKey() != folder.cacheKey()) return
        if (current.searchQuery.isNotBlank() || current.searchParams.isActive()) return
        _state.value = current.copy(
            messages = page.messages,
            nextCursor = page.nextCursor,
            loading = false,
        )
    }

    private suspend fun isFolderStale(session: AuthSession, folder: MailFolderEntry): Boolean {
        val syncedAt = cacheRepository?.folderSyncedAt(session, folder) ?: return true
        return System.currentTimeMillis() - syncedAt > FOLDER_CACHE_TTL_MS
    }

    private fun removeMessageFromCaches(session: AuthSession, messageId: Int) {
        folderCache.replaceAll { _, page ->
            page.copy(messages = page.messages.filter { it.id != messageId })
        }
        viewModelScope.launch(Dispatchers.IO) {
            cacheRepository?.removeMessage(session, messageId)
            cacheRepository?.removeMessageFromFolders(session, messageId)
            _state.value.selectedFolder?.let { folder ->
                folderCache[folder.cacheKey()]?.let { page ->
                    cacheRepository?.saveFolderPage(session, folder, page)
                }
            }
        }
    }

    private fun findMessageFlags(messageId: Int): MailMessageFlags? {
        return _state.value.messageDetail?.takeIf { it.id == messageId }?.flags
            ?: _state.value.messages.find { it.id == messageId }?.flags
    }

    private fun updateMessageFlags(
        session: AuthSession,
        messageId: Int,
        seen: Boolean? = null,
        flagged: Boolean? = null,
        important: Boolean? = null,
    ) {
        fun updateMessage(msg: MailMessage): MailMessage {
            if (msg.id != messageId) return msg
            return msg.copy(
                flags = msg.flags.copy(
                    seen = seen ?: msg.flags.seen,
                    flagged = flagged ?: msg.flags.flagged,
                    important = important ?: msg.flags.important,
                ),
            )
        }
        _state.value = _state.value.copy(messages = _state.value.messages.map(::updateMessage))
        folderCache.replaceAll { _, page ->
            page.copy(messages = page.messages.map(::updateMessage))
        }
        viewModelScope.launch(Dispatchers.IO) {
            cacheRepository?.updateMessageInFolders(session, messageId, ::updateMessage)
            _state.value.selectedFolder?.let { folder ->
                folderCache[folder.cacheKey()]?.let { page ->
                    cacheRepository?.saveFolderPage(session, folder, page)
                }
            }
        }
    }

    private fun fail(t: Throwable) {
        _state.value = _state.value.copy(
            loading = false,
            error = t.message,
            unauthorized = t is UnauthorizedException,
        )
    }

    // --- «Создать задачу/событие из письма» (паритет живого веба) ---

    fun openCreateTaskSheet(session: AuthSession) {
        _state.update { it.copy(createTaskSheetOpen = true, createTargets = emptyList()) }
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                ru.forbion.f7cloud.feature.tasks.TasksRepository()
                    .listTaskLists(session)
                    .map { MailTargetList(it.href, it.displayName) }
            }.onSuccess { lists ->
                _state.update { it.copy(createTargets = lists) }
            }.onFailure { t ->
                _state.update { it.copy(createTaskSheetOpen = false, error = t.message) }
            }
        }
    }

    fun openCreateEventSheet(session: AuthSession) {
        _state.update { it.copy(createEventSheetOpen = true, createTargets = emptyList()) }
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                ru.forbion.f7cloud.feature.calendar.CalendarRepository()
                    .listCalendars(session)
                    .map { MailTargetList(it.href, it.displayName) }
            }.onSuccess { calendars ->
                _state.update { it.copy(createTargets = calendars) }
            }.onFailure { t ->
                _state.update { it.copy(createEventSheetOpen = false, error = t.message) }
            }
        }
    }

    fun dismissCreateSheets() {
        _state.update { it.copy(createTaskSheetOpen = false, createEventSheetOpen = false) }
    }

    fun createTaskFromMessage(
        session: AuthSession,
        listHref: String,
        summary: String,
        description: String,
        due: java.time.LocalDate?,
    ) {
        _state.update { it.copy(creatingFromMessage = true) }
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                ru.forbion.f7cloud.feature.tasks.TasksRepository().createTask(
                    session = session,
                    listHref = listHref,
                    summary = summary,
                    dueDate = due,
                    description = description,
                )
            }.onSuccess {
                _state.update {
                    it.copy(creatingFromMessage = false, createTaskSheetOpen = false, error = null)
                }
            }.onFailure { t ->
                _state.update { it.copy(creatingFromMessage = false, error = t.message) }
            }
        }
    }

    fun createEventFromMessage(
        session: AuthSession,
        calendarHref: String,
        title: String,
        description: String,
        date: java.time.LocalDate,
        startTime: String,
        endTime: String,
        allDay: Boolean,
    ) {
        _state.update { it.copy(creatingFromMessage = true) }
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                ru.forbion.f7cloud.feature.calendar.CalendarRepository().saveEvent(
                    session,
                    ru.forbion.f7cloud.feature.calendar.CalendarEventDraft(
                        title = title,
                        date = date,
                        startTime = startTime,
                        endTime = endTime,
                        allDay = allDay,
                        description = description,
                        calendarHref = calendarHref,
                    ),
                )
            }.onSuccess {
                _state.update {
                    it.copy(creatingFromMessage = false, createEventSheetOpen = false, error = null)
                }
            }.onFailure { t ->
                _state.update { it.copy(creatingFromMessage = false, error = t.message) }
            }
        }
    }

    // --- Фильтры почты (общие с вебом, API /api/filter/{accountId}) ---

    fun openSieveFilters(session: AuthSession, accountId: Int) {
        _state.update {
            it.copy(sieveAccountId = accountId, sieveRules = emptyList(), sieveLoading = true, sieveError = null)
        }
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                repository.getMailFilters(session, accountId)
            }.onSuccess { rules ->
                _state.update { it.copy(sieveRules = rules, sieveLoading = false) }
            }.onFailure { t ->
                _state.update {
                    it.copy(sieveLoading = false, sieveError = t.message ?: "Фильтры недоступны")
                }
            }
        }
    }

    fun closeSieveFilters() {
        _state.update { it.copy(sieveAccountId = null, sieveError = null) }
    }

    fun addFilterRule(session: AuthSession, rule: MailFilterRule) {
        saveFilterRules(session) { it + rule }
    }

    fun updateFilterRule(session: AuthSession, index: Int, rule: MailFilterRule) {
        saveFilterRules(session) { rules ->
            rules.toMutableList().also { if (index in it.indices) it[index] = rule }
        }
    }

    fun deleteFilterRule(session: AuthSession, index: Int) {
        saveFilterRules(session) { rules ->
            rules.toMutableList().also { if (index in it.indices) it.removeAt(index) }
        }
    }

    fun toggleFilterRule(session: AuthSession, index: Int, enable: Boolean) {
        saveFilterRules(session) { rules ->
            rules.toMutableList().also {
                if (index in it.indices) it[index] = it[index].withEnable(enable)
            }
        }
    }

    private fun saveFilterRules(
        session: AuthSession,
        transform: (List<MailFilterRule>) -> List<MailFilterRule>,
    ) {
        val accountId = _state.value.sieveAccountId ?: return
        // Правки применяем к ТЕКУЩЕМУ локальному списку (он же только что с сервера);
        // сервер хранит фильтры единым массивом — PUT перезаписывает секцию приложения Mail.
        val rules = transform(_state.value.sieveRules)
        _state.update { it.copy(sieveSaving = true, sieveError = null) }
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                repository.putMailFilters(session, accountId, rules)
                repository.getMailFilters(session, accountId)
            }.onSuccess { fresh ->
                _state.update { it.copy(sieveRules = fresh, sieveSaving = false) }
            }.onFailure { t ->
                _state.update { it.copy(sieveSaving = false, sieveError = t.message ?: "Ошибка сохранения") }
            }
        }
    }

    companion object {
        private const val MARK_READ_DELAY_MS = 2000L
        private const val FOLDER_CACHE_TTL_MS = 15 * 60 * 1000L
        private const val MESSAGE_PREFETCH_LIMIT = 60
        private const val PREFETCH_CONCURRENCY = 3

        private fun normalizePage(page: MailMessagesPage): MailMessagesPage {
            if (page.messages.size < MailRepository.PAGE_SIZE) {
                return page.copy(nextCursor = null)
            }
            val cursor = page.messages.lastOrNull()?.dateInt?.takeIf { it > 0 }
            return page.copy(nextCursor = cursor)
        }
    }
}
