/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.android.systemui.user.domain.interactor

import android.annotation.UserIdInt
import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.pm.UserInfo
import android.os.RemoteException
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
import android.view.WindowManagerGlobal
import android.widget.Toast
import com.android.internal.logging.UiEventLogger
import com.android.systemui.GuestResetOrExitSessionReceiver
import com.android.systemui.GuestResumeSessionReceiver
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.QSUserSwitcherEvent
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.user.domain.model.ShowDialogRequestModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext

/** Encapsulates business logic to interact with guest user data and systems. */
@SysUISingleton
class GuestUserInteractor
@Inject
constructor(
    @Application private val applicationContext: Context,
    @Application private val applicationScope: CoroutineScope,
    @Main private val mainDispatcher: CoroutineDispatcher,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val manager: UserManager,
    private val repository: UserRepository,
    private val deviceProvisionedController: DeviceProvisionedController,
    private val devicePolicyManager: DevicePolicyManager,
    private val refreshUsersScheduler: RefreshUsersScheduler,
    private val uiEventLogger: UiEventLogger,
    resumeSessionReceiver: GuestResumeSessionReceiver,
    resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver,
) {
    /** Whether the device is configured to always have a guest user available. */
    val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated

    /** Whether the guest user is currently being reset. */
    val isGuestUserResetting: Boolean = repository.isGuestUserResetting

    init {
        resumeSessionReceiver.register()
        resetOrExitSessionReceiver.register()
    }

    /** Notifies that the device has finished booting. */
    fun onDeviceBootCompleted() {
        applicationScope.launch {
            if (isDeviceAllowedToAddGuest()) {
                guaranteePresent()
                return@launch
            }

            suspendCancellableCoroutine<Unit> { continuation ->
                val callback =
                    object : DeviceProvisionedController.DeviceProvisionedListener {
                        override fun onDeviceProvisionedChanged() {
                            continuation.resumeWith(Result.success(Unit))
                            deviceProvisionedController.removeCallback(this)
                        }
                    }

                deviceProvisionedController.addCallback(callback)
            }

            if (isDeviceAllowedToAddGuest()) {
                guaranteePresent()
            }
        }
    }

    /** Creates a guest user and switches to it. */
    fun createAndSwitchTo(
        showDialog: (ShowDialogRequestModel) -> Unit,
        dismissDialog: () -> Unit,
        selectUser: (userId: Int) -> Unit,
    ) {
        applicationScope.launch {
            val newGuestUserId = create(showDialog, dismissDialog)
            if (newGuestUserId != UserHandle.USER_NULL) {
                selectUser(newGuestUserId)
            }
        }
    }

    /** Exits the guest user, switching back to the last non-guest user or to the default user. */
    fun exit(
        @UserIdInt guestUserId: Int,
        @UserIdInt targetUserId: Int,
        forceRemoveGuestOnExit: Boolean,
        showDialog: (ShowDialogRequestModel) -> Unit,
        dismissDialog: () -> Unit,
        switchUser: (userId: Int) -> Unit,
    ) {
        val currentUserInfo = repository.getSelectedUserInfo()
        if (currentUserInfo.id != guestUserId) {
            Log.w(
                TAG,
                "User requesting to start a new session ($guestUserId) is not current user" +
                    " (${currentUserInfo.id})"
            )
            return
        }

        if (!currentUserInfo.isGuest) {
            Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
            return
        }

        applicationScope.launch {
            var newUserId = repository.mainUserId
            if (targetUserId == UserHandle.USER_NULL) {
                // When a target user is not specified switch to last non guest user:
                val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
                if (lastSelectedNonGuestUserHandle != repository.mainUserId) {
                    val info =
                        withContext(backgroundDispatcher) {
                            manager.getUserInfo(lastSelectedNonGuestUserHandle)
                        }
                    if (info != null && info.isEnabled && info.supportsSwitchTo()) {
                        newUserId = info.id
                    }
                }
            } else {
                newUserId = targetUserId
            }

            if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) {
                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE)
                remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser)
            } else {
                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH)
                switchUser(newUserId)
            }
        }
    }

    /**
     * Guarantees that the guest user is present on the device, creating it if needed and if allowed
     * to.
     */
    suspend fun guaranteePresent() {
        if (!isDeviceAllowedToAddGuest()) {
            return
        }

        val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() }
        if (guestUser == null) {
            scheduleCreation()
        }
    }

    /** Removes the guest user from the device. */
    suspend fun remove(
        @UserIdInt guestUserId: Int,
        @UserIdInt targetUserId: Int,
        showDialog: (ShowDialogRequestModel) -> Unit,
        dismissDialog: () -> Unit,
        switchUser: (userId: Int) -> Unit,
    ) {
        val currentUser: UserInfo = repository.getSelectedUserInfo()
        if (currentUser.id != guestUserId) {
            Log.w(
                TAG,
                "User requesting to start a new session ($guestUserId) is not current user" +
                    " ($currentUser.id)"
            )
            return
        }

        if (!currentUser.isGuest) {
            Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
            return
        }

        val marked =
            withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) }
        if (!marked) {
            Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId")
            return
        }

        if (targetUserId == UserHandle.USER_NULL) {
            // Create a new guest in the foreground, and then immediately switch to it
            val newGuestId = create(showDialog, dismissDialog)
            if (newGuestId == UserHandle.USER_NULL) {
                Log.e(TAG, "Could not create new guest, switching back to main user")
                val mainUser = withContext(backgroundDispatcher) { manager.mainUser?.identifier }

                mainUser?.let { switchUser(it) }

                withContext(backgroundDispatcher) {
                    manager.removeUserWhenPossible(
                        UserHandle.of(currentUser.id),
                        /* overrideDevicePolicy= */ false
                    )
                }
                try {
                    checkNotNull(WindowManagerGlobal.getWindowManagerService())
                        .lockNow(/* options= */ null)
                } catch (e: RemoteException) {
                    Log.e(
                        TAG,
                        "Couldn't remove guest because ActivityManager or WindowManager is dead"
                    )
                }
                return
            }

            switchUser(newGuestId)

            withContext(backgroundDispatcher) {
                manager.removeUserWhenPossible(
                    UserHandle.of(currentUser.id),
                    /* overrideDevicePolicy= */ false
                )
            }
        } else {
            if (repository.isGuestUserAutoCreated) {
                repository.isGuestUserResetting = true
            }
            switchUser(targetUserId)
            manager.removeUserWhenPossible(
                UserHandle.of(currentUser.id),
                /* overrideDevicePolicy= */ false
            )
        }
    }

    /**
     * Creates the guest user and adds it to the device.
     *
     * @param showDialog A function to invoke to show a dialog.
     * @param dismissDialog A function to invoke to dismiss a dialog.
     * @return The user ID of the newly-created guest user.
     */
    private suspend fun create(
        showDialog: (ShowDialogRequestModel) -> Unit,
        dismissDialog: () -> Unit,
    ): Int {
        return withContext(mainDispatcher) {
            showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
            val guestUserId = createInBackground()
            dismissDialog()
            if (guestUserId != UserHandle.USER_NULL) {
                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD)
            } else {
                Toast.makeText(
                        applicationContext,
                        com.android.settingslib.R.string.add_guest_failed,
                        Toast.LENGTH_SHORT,
                    )
                    .show()
            }

            guestUserId
        }
    }

    /** Schedules the creation of the guest user. */
    private suspend fun scheduleCreation() {
        if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) {
            return
        }

        withContext(backgroundDispatcher) {
            val newGuestUserId = createInBackground()
            repository.isGuestUserCreationScheduled.set(false)
            repository.isGuestUserResetting = false
            if (newGuestUserId == UserHandle.USER_NULL) {
                Log.w(TAG, "Could not create new guest while exiting existing guest")
                // Refresh users so that we still display "Guest" if
                // config_guestUserAutoCreated=true
                refreshUsersScheduler.refreshIfNotPaused()
            }
        }
    }

    /**
     * Creates a guest user and return its multi-user user ID.
     *
     * This method does not check if a guest already exists before it makes a call to [UserManager]
     * to create a new one.
     *
     * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if
     *   the guest couldn't be created.
     */
    @UserIdInt
    private suspend fun createInBackground(): Int {
        return withContext(backgroundDispatcher) {
            try {
                val guestUser = manager.createGuest(applicationContext)
                if (guestUser != null) {
                    guestUser.id
                } else {
                    Log.e(
                        TAG,
                        "Couldn't create guest, most likely because there already exists one!"
                    )
                    UserHandle.USER_NULL
                }
            } catch (e: UserManager.UserOperationException) {
                Log.e(TAG, "Couldn't create guest user!", e)
                UserHandle.USER_NULL
            }
        }
    }

    private fun isDeviceAllowedToAddGuest(): Boolean {
        return deviceProvisionedController.isDeviceProvisioned &&
            !devicePolicyManager.isDeviceManaged
    }

    companion object {
        private const val TAG = "GuestUserInteractor"
    }
}
