/*
 * Copyright 2023, 2024 New Vector Ltd.
 *
 * SPDX-License-Identifier: AGPL-3.0-only
 * Please see LICENSE in the repository root for full details.
 */

package io.element.android.appnav.loggedin

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.RegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

private val pusherTag = LoggerTag("Pusher", LoggerTag.PushLoggerTag)

class LoggedInPresenter @Inject constructor(
    private val matrixClient: MatrixClient,
    private val networkMonitor: NetworkMonitor,
    private val pushService: PushService,
    private val sessionVerificationService: SessionVerificationService,
    private val analyticsService: AnalyticsService,
    private val encryptionService: EncryptionService,
    private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
) : Presenter<LoggedInState> {
    @Composable
    override fun present(): LoggedInState {
        val coroutineScope = rememberCoroutineScope()
        val ignoreRegistrationError by remember {
            pushService.ignoreRegistrationError(matrixClient.sessionId)
        }.collectAsState(initial = false)
        val pusherRegistrationState = remember<MutableState<AsyncData<Unit>>> { mutableStateOf(AsyncData.Uninitialized) }
        LaunchedEffect(Unit) {
            sessionVerificationService.sessionVerifiedStatus
                .onEach { sessionVerifiedStatus ->
                    when (sessionVerifiedStatus) {
                        SessionVerifiedStatus.Unknown -> Unit
                        SessionVerifiedStatus.Verified -> {
                            ensurePusherIsRegistered(pusherRegistrationState)
                        }
                        SessionVerifiedStatus.NotVerified -> {
                            pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.AccountNotVerified())
                        }
                    }
                }
                .launchIn(this)
        }
        val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
        val networkStatus by networkMonitor.connectivity.collectAsState()
        val showSyncSpinner by remember {
            derivedStateOf {
                networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
            }
        }
        var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) }
        LaunchedEffect(Unit) {
            combine(
                sessionVerificationService.sessionVerifiedStatus,
                encryptionService.recoveryStateStateFlow
            ) { verificationState, recoveryState ->
                reportCryptoStatusToAnalytics(verificationState, recoveryState)
            }.launchIn(this)
        }

        fun handleEvent(event: LoggedInEvents) {
            when (event) {
                is LoggedInEvents.CloseErrorDialog -> {
                    pusherRegistrationState.value = AsyncData.Uninitialized
                    if (event.doNotShowAgain) {
                        coroutineScope.launch {
                            pushService.setIgnoreRegistrationError(matrixClient.sessionId, true)
                        }
                    }
                }
                LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
                    // Force the user to log out if they were using the proxy sliding sync and it's no longer available, but native sliding sync is.
                    forceNativeSlidingSyncMigration = !matrixClient.isUsingNativeSlidingSync() &&
                        matrixClient.isNativeSlidingSyncSupported() &&
                        !matrixClient.isSlidingSyncProxySupported()
                }
                LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
                    // Enable native sliding sync if it wasn't already the case
                    enableNativeSlidingSyncUseCase()
                    // Then force the logout
                    matrixClient.logout(userInitiated = true, ignoreSdkError = true)
                }
            }
        }

        return LoggedInState(
            showSyncSpinner = showSyncSpinner,
            pusherRegistrationState = pusherRegistrationState.value,
            ignoreRegistrationError = ignoreRegistrationError,
            forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
            eventSink = ::handleEvent
        )
    }

    private suspend fun ensurePusherIsRegistered(pusherRegistrationState: MutableState<AsyncData<Unit>>) {
        Timber.tag(pusherTag.value).d("Ensure pusher is registered")
        val currentPushProvider = pushService.getCurrentPushProvider()
        val result = if (currentPushProvider == null) {
            Timber.tag(pusherTag.value).d("Register with the first available push provider with at least one distributor")
            val pushProvider = pushService.getAvailablePushProviders()
                .firstOrNull { it.getDistributors().isNotEmpty() }
            // Else fallback to the first available push provider (the list should never be empty)
                ?: pushService.getAvailablePushProviders().firstOrNull()
                ?: return Unit
                    .also { Timber.tag(pusherTag.value).w("No push providers available") }
                    .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoProvidersAvailable()) }
            val distributor = pushProvider.getDistributors().firstOrNull()
                ?: return Unit
                    .also { Timber.tag(pusherTag.value).w("No distributors available") }
                    .also {
                        // In this case, consider the push provider is chosen.
                        pushService.selectPushProvider(matrixClient.sessionId, pushProvider)
                    }
                    .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) }
            pushService.registerWith(matrixClient, pushProvider, distributor)
        } else {
            val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient.sessionId)
            if (currentPushDistributor == null) {
                Timber.tag(pusherTag.value).d("Register with the first available distributor")
                val distributor = currentPushProvider.getDistributors().firstOrNull()
                    ?: return Unit
                        .also { Timber.tag(pusherTag.value).w("No distributors available") }
                        .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) }
                pushService.registerWith(matrixClient, currentPushProvider, distributor)
            } else {
                Timber.tag(pusherTag.value).d("Re-register with the current distributor")
                pushService.registerWith(matrixClient, currentPushProvider, currentPushDistributor)
            }
        }
        result.fold(
            onSuccess = {
                Timber.tag(pusherTag.value).d("Pusher registered")
                pusherRegistrationState.value = AsyncData.Success(Unit)
            },
            onFailure = {
                Timber.tag(pusherTag.value).e(it, "Failed to register pusher")
                if (it is RegistrationFailure) {
                    pusherRegistrationState.value = AsyncData.Failure(
                        PusherRegistrationFailure.RegistrationFailure(it.clientException, it.isRegisteringAgain)
                    )
                } else {
                    pusherRegistrationState.value = AsyncData.Failure(it)
                }
            }
        )
    }

    private fun reportCryptoStatusToAnalytics(verificationState: SessionVerifiedStatus, recoveryState: RecoveryState) {
        // Update first the user property, to store the current status for that posthog user
        val userVerificationState = verificationState.toAnalyticsUserPropertyValue()
        val userRecoveryState = recoveryState.toAnalyticsUserPropertyValue()
        if (userRecoveryState != null && userVerificationState != null) {
            // we want to report when both value are known (if one is unknown we wait until we have them both)
            analyticsService.updateUserProperties(
                UserProperties(
                    verificationState = userVerificationState,
                    recoveryState = userRecoveryState
                )
            )
        }

        // Also report when there is a change in the state, to be able to track the changes
        val changeVerificationState = verificationState.toAnalyticsStateChangeValue()
        val changeRecoveryState = recoveryState.toAnalyticsStateChangeValue()
        if (changeVerificationState != null && changeRecoveryState != null) {
            analyticsService.capture(CryptoSessionStateChange(changeRecoveryState, changeVerificationState))
        }
    }
}
