/*
 * Copyright (C) 2023 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.credentialmanager

import android.app.Activity
import android.os.IBinder
import android.text.TextUtils
import android.util.Log
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.android.credentialmanager.common.BaseEntry
import com.android.credentialmanager.common.Constants
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.createflow.ActiveEntry
import com.android.credentialmanager.createflow.isFlowAutoSelectable
import com.android.credentialmanager.createflow.CreateCredentialUiState
import com.android.credentialmanager.createflow.CreateScreenState
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.GetScreenState
import com.android.credentialmanager.logging.LifecycleEvent
import com.android.credentialmanager.logging.UIMetrics
import com.android.internal.logging.UiEventLogger.UiEventEnum

/** One and only one of create or get state can be active at any given time. */
data class UiState(
    val createCredentialUiState: CreateCredentialUiState?,
    val getCredentialUiState: GetCredentialUiState?,
    val selectedEntry: BaseEntry? = null,
    val providerActivityState: ProviderActivityState = ProviderActivityState.NOT_APPLICABLE,
    val dialogState: DialogState = DialogState.ACTIVE,
    // True if the UI has one and only one auto selectable entry. Its provider activity will be
    // launched immediately, and canceling it will cancel the whole UI flow.
    val isAutoSelectFlow: Boolean = false,
    val cancelRequestState: CancelUiRequestState?,
    val isInitialRender: Boolean,
)

data class CancelUiRequestState(
    val appDisplayName: String?,
)

class CredentialSelectorViewModel(
    private var credManRepo: CredentialManagerRepo,
    private val userConfigRepo: UserConfigRepo,
) : ViewModel() {
    var uiState by mutableStateOf(credManRepo.initState())
        private set

    var uiMetrics: UIMetrics = UIMetrics()

    init {
        uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_INIT,
            credManRepo.requestInfo?.appPackageName)
    }

    /**************************************************************************/
    /*****                       Shared Callbacks                         *****/
    /**************************************************************************/
    fun onUserCancel() {
        Log.d(Constants.LOG_TAG, "User cancelled, finishing the ui")
        credManRepo.onUserCancel()
        uiState = uiState.copy(dialogState = DialogState.COMPLETE)
    }

    fun onInitialRenderComplete() {
        uiState = uiState.copy(isInitialRender = false)
    }

    fun onCancellationUiRequested(appDisplayName: String?) {
        uiState = uiState.copy(cancelRequestState = CancelUiRequestState(appDisplayName))
    }

    /** Close the activity and don't report anything to the backend.
     *  Example use case is the no-auth-info snackbar where the activity should simply display the
     *  UI and then be dismissed. */
    fun silentlyFinishActivity() {
        Log.d(Constants.LOG_TAG, "Silently finishing the ui")
        uiState = uiState.copy(dialogState = DialogState.COMPLETE)
    }

    fun onNewCredentialManagerRepo(credManRepo: CredentialManagerRepo) {
        this.credManRepo = credManRepo
        uiState = credManRepo.initState().copy(isInitialRender = false)

        if (this.credManRepo.requestInfo?.token != credManRepo.requestInfo?.token) {
            this.uiMetrics.resetInstanceId()
            this.uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_NEW_REQUEST,
                credManRepo.requestInfo?.appPackageName)
        }
    }

    fun launchProviderUi(
        launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
    ) {
        val entry = uiState.selectedEntry
        if (entry != null && entry.pendingIntent != null) {
            Log.d(Constants.LOG_TAG, "Launching provider activity")
            uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING)
            val entryIntent = entry.fillInIntent
            entryIntent?.putExtra(Constants.IS_AUTO_SELECTED_KEY, uiState.isAutoSelectFlow)
            val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
                .setFillInIntent(entryIntent).build()
            try {
                launcher.launch(intentSenderRequest)
            } catch (e: Exception) {
                Log.w(Constants.LOG_TAG, "Failed to launch provider UI: $e")
                onInternalError()
            }
        } else {
            Log.d(Constants.LOG_TAG, "No provider UI to launch")
            onInternalError()
        }
    }

    fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
        val entry = uiState.selectedEntry
        val resultCode = providerActivityResult.resultCode
        val resultData = providerActivityResult.data
        if (resultCode == Activity.RESULT_CANCELED) {
            // Re-display the CredMan UI if the user canceled from the provider UI, or cancel
            // the UI if this is the auto select flow.
            if (uiState.isAutoSelectFlow) {
                Log.d(Constants.LOG_TAG, "The auto selected provider activity was cancelled," +
                    " ending the credential manager activity.")
                onUserCancel()
            } else {
                Log.d(Constants.LOG_TAG, "The provider activity was cancelled," +
                    " re-displaying our UI.")
                uiState = uiState.copy(
                    selectedEntry = null,
                    providerActivityState = ProviderActivityState.NOT_APPLICABLE,
                )
            }
        } else {
            if (entry != null) {
                Log.d(
                    Constants.LOG_TAG, "Got provider activity result: {provider=" +
                    "${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}" +
                    ", resultCode=$resultCode, resultData=$resultData}"
                )
                credManRepo.onOptionSelected(
                    entry.providerId, entry.entryKey, entry.entrySubkey,
                    resultCode, resultData,
                )
                if (entry.shouldTerminateUiUponSuccessfulProviderResult) {
                    uiState = uiState.copy(dialogState = DialogState.COMPLETE)
                }
            } else {
                Log.w(Constants.LOG_TAG,
                    "Illegal state: received a provider result but found no matching entry.")
                onInternalError()
            }
        }
    }

    fun onLastLockedAuthEntryNotFoundError() {
        Log.d(Constants.LOG_TAG, "Unable to find the last unlocked entry")
        onInternalError()
    }

    fun onIllegalUiState(errorMessage: String) {
        Log.w(Constants.LOG_TAG, errorMessage)
        onInternalError()
    }

    private fun onInternalError() {
        Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state")
        this.uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_INTERNAL_ERROR,
            credManRepo.requestInfo?.appPackageName)
        credManRepo.onParsingFailureCancel()
        uiState = uiState.copy(dialogState = DialogState.COMPLETE)
    }

    /** Return true if the current UI's request token matches the UI cancellation request token. */
    fun shouldCancelCurrentUi(cancelRequestToken: IBinder): Boolean {
        return credManRepo.requestInfo?.token?.equals(cancelRequestToken) ?: false
    }

    /**************************************************************************/
    /*****                      Get Flow Callbacks                        *****/
    /**************************************************************************/
    fun getFlowOnEntrySelected(entry: BaseEntry) {
        Log.d(Constants.LOG_TAG, "credential selected: {provider=${entry.providerId}" +
            ", key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
        uiState = if (entry.pendingIntent != null) {
            uiState.copy(
                selectedEntry = entry,
                providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
            )
        } else {
            credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey)
            uiState.copy(dialogState = DialogState.COMPLETE)
        }
    }

    fun getFlowOnConfirmEntrySelected() {
        val activeEntry = uiState.getCredentialUiState?.activeEntry
        if (activeEntry != null) {
            getFlowOnEntrySelected(activeEntry)
        } else {
            Log.d(Constants.LOG_TAG,
                "Illegal state: confirm is pressed but activeEntry isn't set.")
            onInternalError()
        }
    }

    fun getFlowOnMoreOptionSelected() {
        Log.d(Constants.LOG_TAG, "More Option selected")
        uiState = uiState.copy(
            getCredentialUiState = uiState.getCredentialUiState?.copy(
                currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS
            )
        )
    }

    fun getFlowOnMoreOptionOnSnackBarSelected(isNoAccount: Boolean) {
        Log.d(Constants.LOG_TAG, "More Option on snackBar selected")
        uiState = uiState.copy(
            getCredentialUiState = uiState.getCredentialUiState?.copy(
                currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS,
                isNoAccount = isNoAccount,
            ),
            isInitialRender = true,
        )
    }

    fun getFlowOnBackToHybridSnackBarScreen() {
        uiState = uiState.copy(
            getCredentialUiState = uiState.getCredentialUiState?.copy(
                currentScreenState = GetScreenState.REMOTE_ONLY
            )
        )
    }

    fun getFlowOnBackToPrimarySelectionScreen() {
        uiState = uiState.copy(
            getCredentialUiState = uiState.getCredentialUiState?.copy(
                currentScreenState = GetScreenState.PRIMARY_SELECTION
            )
        )
    }

    /**************************************************************************/
    /*****                     Create Flow Callbacks                      *****/
    /**************************************************************************/
    fun createFlowOnConfirmIntro() {
        userConfigRepo.setIsPasskeyFirstUse(false)
        val prevUiState = uiState.createCredentialUiState
        if (prevUiState == null) {
            Log.d(Constants.LOG_TAG, "Encountered unexpected null create ui state")
            onInternalError()
            return
        }
        val newScreenState = CreateFlowUtils.toCreateScreenState(
            createOptionSize = prevUiState.sortedCreateOptionsPairs.size,
            isOnPasskeyIntroStateAlready = true,
            requestDisplayInfo = prevUiState.requestDisplayInfo,
            remoteEntry = prevUiState.remoteEntry,
            isPasskeyFirstUse = true,
        )
        if (newScreenState == null) {
            Log.d(Constants.LOG_TAG, "Unexpected: couldn't resolve new screen state")
            onInternalError()
            return
        }
        val newCreateCredentialUiState = prevUiState.copy(
            currentScreenState = newScreenState,
        )
        val isFlowAutoSelectable = isFlowAutoSelectable(newCreateCredentialUiState)
        uiState = uiState.copy(
            createCredentialUiState = newCreateCredentialUiState,
            isAutoSelectFlow = isFlowAutoSelectable,
            providerActivityState =
            if (isFlowAutoSelectable) ProviderActivityState.READY_TO_LAUNCH
            else ProviderActivityState.NOT_APPLICABLE,
            selectedEntry =
            if (isFlowAutoSelectable) newCreateCredentialUiState.activeEntry?.activeEntryInfo
            else null,
        )
    }

    fun createFlowOnMoreOptionsSelectedOnCreationSelection() {
        uiState = uiState.copy(
            createCredentialUiState = uiState.createCredentialUiState?.copy(
                currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
            )
        )
    }

    fun createFlowOnBackCreationSelectionButtonSelected() {
        uiState = uiState.copy(
            createCredentialUiState = uiState.createCredentialUiState?.copy(
                currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
            )
        )
    }

    fun createFlowOnBackPasskeyIntroButtonSelected() {
        uiState = uiState.copy(
            createCredentialUiState = uiState.createCredentialUiState?.copy(
                currentScreenState = CreateScreenState.PASSKEY_INTRO,
            )
        )
    }

    fun createFlowOnEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) {
        uiState = uiState.copy(
            createCredentialUiState = uiState.createCredentialUiState?.copy(
                currentScreenState =
                if (uiState.createCredentialUiState?.requestDisplayInfo?.userSetDefaultProviderIds
                        ?.contains(activeEntry.activeProvider.id) ?: true ||
                    !(uiState.createCredentialUiState?.foundCandidateFromUserDefaultProvider
                    ?: false) ||
                    !TextUtils.isEmpty(uiState.createCredentialUiState?.requestDisplayInfo
                        ?.appPreferredDefaultProviderId))
                    CreateScreenState.CREATION_OPTION_SELECTION
                else CreateScreenState.DEFAULT_PROVIDER_CONFIRMATION,
                activeEntry = activeEntry
            )
        )
    }

    fun createFlowOnLaunchSettings() {
        credManRepo.onSettingLaunchCancel()
        uiState = uiState.copy(dialogState = DialogState.CANCELED_FOR_SETTINGS)
    }

    fun createFlowOnLearnMore() {
        uiState = uiState.copy(
            createCredentialUiState = uiState.createCredentialUiState?.copy(
                currentScreenState = CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO,
            )
        )
    }

    fun createFlowOnUseOnceSelected() {
        uiState = uiState.copy(
            createCredentialUiState = uiState.createCredentialUiState?.copy(
                currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
            )
        )
    }

    fun createFlowOnEntrySelected(selectedEntry: BaseEntry) {
        val providerId = selectedEntry.providerId
        val entryKey = selectedEntry.entryKey
        val entrySubkey = selectedEntry.entrySubkey
        Log.d(
            Constants.LOG_TAG, "Option selected for entry: " +
            " {provider=$providerId, key=$entryKey, subkey=$entrySubkey")
        if (selectedEntry.pendingIntent != null) {
            uiState = uiState.copy(
                selectedEntry = selectedEntry,
                providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
            )
        } else {
            credManRepo.onOptionSelected(
                providerId,
                entryKey,
                entrySubkey
            )
            uiState = uiState.copy(dialogState = DialogState.COMPLETE)
        }
    }

    fun createFlowOnConfirmEntrySelected() {
        val selectedEntry = uiState.createCredentialUiState?.activeEntry?.activeEntryInfo
        if (selectedEntry != null) {
            createFlowOnEntrySelected(selectedEntry)
        } else {
            Log.d(Constants.LOG_TAG,
                "Unexpected: confirm is pressed but no active entry exists.")
            onInternalError()
        }
    }

    @Composable
    fun logUiEvent(uiEventEnum: UiEventEnum) {
        this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.appPackageName)
    }
}