package ch.threema.app.startup

import ch.threema.app.startup.models.AppSystem
import ch.threema.app.startup.models.SystemStatus
import ch.threema.app.systemupdates.SystemUpdateState
import ch.threema.base.utils.getThreemaLogger
import ch.threema.common.DelegateStateFlow
import ch.threema.common.combineStates
import ch.threema.common.mapState
import ch.threema.common.stateFlowOf
import ch.threema.storage.DatabaseState
import kotlin.collections.component1
import kotlin.collections.component2
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update

private val logger = getThreemaLogger("AppStartupMonitorImpl")

class AppStartupMonitorImpl : AppStartupMonitor {
    private val unlockedMasterKeyStateFlow = MutableStateFlow(SystemStatus.PENDING)
    private val remoteSecretStateFlow = MutableStateFlow(SystemStatus.UNKNOWN)
    private val databaseStateFlow = DelegateStateFlow(stateFlowOf(SystemStatus.UNKNOWN))
    private val systemUpdateStateFlow = DelegateStateFlow(stateFlowOf(SystemStatus.UNKNOWN))
    private val systemStatuses = DelegateStateFlow(createInitialPendingSystemsFlow())

    private fun createInitialPendingSystemsFlow() =
        remoteSecretStateFlow.mapState { remoteSecretState ->
            mapOf(
                AppSystem.REMOTE_SECRET to remoteSecretState,
                AppSystem.UNLOCKED_MASTER_KEY to SystemStatus.PENDING,
                AppSystem.DATABASE_UPDATES to SystemStatus.UNKNOWN,
                AppSystem.SYSTEM_UPDATES to SystemStatus.UNKNOWN,
            )
        }

    private val errors = MutableStateFlow(emptySet<AppStartupError>())

    suspend fun whileFetchingRemoteSecret(block: suspend () -> Unit) {
        remoteSecretStateFlow.value = SystemStatus.PENDING
        block()
        remoteSecretStateFlow.value = SystemStatus.READY
    }

    fun onMasterKeyUnlocked(
        databaseStateFlow: StateFlow<DatabaseState>,
        systemUpdateStateFlow: StateFlow<SystemUpdateState>,
    ) {
        this.unlockedMasterKeyStateFlow.value = SystemStatus.READY
        this.databaseStateFlow.delegate = databaseStateFlow.mapState { databaseState ->
            databaseState.toSystemStatus()
        }
        this.systemUpdateStateFlow.delegate = systemUpdateStateFlow.mapState { systemUpdateState ->
            systemUpdateState.toSystemStatus()
        }

        systemStatuses.delegate = combineStates(
            databaseStateFlow,
            systemUpdateStateFlow,
        ) { databaseState, systemUpdateState ->
            buildMap {
                put(AppSystem.UNLOCKED_MASTER_KEY, SystemStatus.READY)
                put(AppSystem.REMOTE_SECRET, SystemStatus.READY)
                put(AppSystem.DATABASE_UPDATES, databaseState.toSystemStatus())
                put(AppSystem.SYSTEM_UPDATES, systemUpdateState.toSystemStatus())
            }
        }

        // the master key becoming unlocked implies that the Remote Secret is also ready
        this.remoteSecretStateFlow.value = SystemStatus.READY
    }

    private fun DatabaseState.toSystemStatus() =
        when (this) {
            DatabaseState.INIT,
            DatabaseState.PREPARING,
            -> SystemStatus.PENDING
            DatabaseState.READY -> SystemStatus.READY
        }

    private fun SystemUpdateState.toSystemStatus() =
        when (this) {
            SystemUpdateState.INIT,
            SystemUpdateState.PREPARING,
            -> SystemStatus.PENDING
            SystemUpdateState.READY -> SystemStatus.READY
        }

    fun onMasterKeyLocked() {
        // Reset to a static state flow first so that the following updates appear atomic from the outside
        systemStatuses.delegate = stateFlowOf(
            mapOf(
                AppSystem.UNLOCKED_MASTER_KEY to SystemStatus.PENDING,
                AppSystem.REMOTE_SECRET to SystemStatus.UNKNOWN,
                AppSystem.DATABASE_UPDATES to SystemStatus.UNKNOWN,
                AppSystem.SYSTEM_UPDATES to SystemStatus.UNKNOWN,
            ),
        )

        unlockedMasterKeyStateFlow.value = SystemStatus.PENDING
        remoteSecretStateFlow.update { previousValue ->
            if (previousValue == SystemStatus.PENDING) {
                // If we're already in PENDING state, it needs to be preserved
                SystemStatus.PENDING
            } else {
                SystemStatus.UNKNOWN
            }
        }
        databaseStateFlow.delegate = stateFlowOf(SystemStatus.UNKNOWN)
        systemUpdateStateFlow.delegate = stateFlowOf(SystemStatus.UNKNOWN)
        systemStatuses.delegate = createInitialPendingSystemsFlow()
    }

    override fun watchSystems() = systemStatuses

    override fun watchPendingSystems(): StateFlow<Set<AppSystem>> =
        systemStatuses.mapState { statuses ->
            buildSet {
                statuses.forEach { (system, status) ->
                    if (status == SystemStatus.PENDING) {
                        add(system)
                    }
                }
            }
        }

    override fun isReady(): Boolean =
        systemStatuses.value.all { (_, status) -> status == SystemStatus.READY } && errors.value.isEmpty()

    override fun isReady(system: AppSystem): Boolean =
        systemStatuses.value[system] == SystemStatus.READY && errors.value.isEmpty()

    override suspend fun awaitSystem(system: AppSystem) {
        systemStatuses.first { statuses ->
            statuses[system] == SystemStatus.READY
        }
        errors.first { it.isEmpty() }
    }

    /**
     * Waits for all systems to be ready.
     * If an app startup error is reported, this will never return, as the app is never considered "ready" in that case.
     */
    override suspend fun awaitAll() {
        // We return immediately if the app is already ready, which allows us to avoid a coroutine suspension
        if (isReady()) {
            return
        }
        systemStatuses.first { statuses ->
            statuses.all { (_, status) ->
                status == SystemStatus.READY
            }
        }
        errors.first { it.isEmpty() }
    }

    override fun watchErrors(): StateFlow<Set<AppStartupError>> = errors

    override fun hasErrors() = errors.value.isNotEmpty()

    /**
     * Report an error that occurred during the startup sequence of the app, which prevents the app from being used.
     */
    fun reportAppStartupError(error: AppStartupError) {
        logger.warn("Startup error reported, {}", error)
        errors.update { it + error }
    }

    fun reportUnexpectedAppStartupError(code: String) {
        reportAppStartupError(AppStartupError.Unexpected(code))
    }

    fun clearTemporaryStartupErrors() {
        errors.update { it.filterNot(AppStartupError::isTemporary).toSet() }
    }
}
