/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.settings.logins.controller

import android.util.Log
import androidx.navigation.NavController
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.concept.storage.Login
import mozilla.components.service.sync.logins.InvalidRecordException
import mozilla.components.service.sync.logins.LoginsStorageException
import mozilla.components.service.sync.logins.NoSuchRecordException
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.logins.LoginsAction
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections
import org.mozilla.fenix.settings.logins.mapToSavedLogin

/**
 * Controller for all saved logins interactions with the password storage component
 */
open class SavedLoginsStorageController(
    private val passwordsStorage: SyncableLoginsStorage,
    private val viewLifecycleScope: CoroutineScope,
    private val navController: NavController,
    private val loginsFragmentStore: LoginsFragmentStore,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {

    private suspend fun getLogin(loginId: String): Login? = passwordsStorage.get(loginId)

    fun delete(loginId: String) {
        var deleteLoginJob: Deferred<Boolean>? = null
        val deleteJob = viewLifecycleScope.launch(ioDispatcher) {
            deleteLoginJob = async {
                passwordsStorage.delete(loginId)
            }
            deleteLoginJob?.await()
            withContext(Dispatchers.Main) {
                navController.popBackStack(R.id.savedLoginsFragment, false)
            }
        }
        deleteJob.invokeOnCompletion {
            if (it is CancellationException) {
                deleteLoginJob?.cancel()
            }
        }
    }

    fun save(loginId: String, usernameText: String, passwordText: String) {
        var saveLoginJob: Deferred<Unit>? = null
        viewLifecycleScope.launch(ioDispatcher) {
            saveLoginJob = async {
                // must retrieve from storage to get the httpsRealm and formActionOrigin
                val oldLogin = passwordsStorage.get(loginId)

                // Update requires a Login type, which needs at least one of
                // httpRealm or formActionOrigin
                val loginToSave = Login(
                    guid = loginId,
                    origin = oldLogin?.origin!!,
                    username = usernameText, // new value
                    password = passwordText, // new value
                    httpRealm = oldLogin.httpRealm,
                    formActionOrigin = oldLogin.formActionOrigin
                )

                save(loginToSave)
                syncAndUpdateList(loginToSave)
            }
            saveLoginJob?.await()
            withContext(Dispatchers.Main) {
                val directions =
                    EditLoginFragmentDirections.actionEditLoginFragmentToLoginDetailFragment(
                        loginId
                    )
                navController.navigate(directions)
            }
        }
        saveLoginJob?.invokeOnCompletion {
            if (it is CancellationException) {
                saveLoginJob?.cancel()
            }
        }
    }

    private suspend fun save(loginToSave: Login) {
        try {
            passwordsStorage.update(loginToSave)
        } catch (loginException: LoginsStorageException) {
            when (loginException) {
                is NoSuchRecordException,
                is InvalidRecordException -> {
                    Log.e(
                        "Edit login",
                        "Failed to save edited login.", loginException
                    )
                }
                else -> Log.e(
                    "Edit login",
                    "Failed to save edited login.", loginException
                )
            }
        }
    }

    private fun syncAndUpdateList(updatedLogin: Login) {
        val login = updatedLogin.mapToSavedLogin()
        loginsFragmentStore.dispatch(
            LoginsAction.UpdateLoginsList(
                listOf(login)
            )
        )
    }

    fun findPotentialDuplicates(loginId: String) {
        var deferredLogin: Deferred<List<Login>>? = null
        val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) {
            deferredLogin = async {
                val login = getLogin(loginId)
                passwordsStorage.getPotentialDupesIgnoringUsername(login!!)
            }
            val fetchedDuplicatesList = deferredLogin?.await()
            fetchedDuplicatesList?.let { list ->
                withContext(Dispatchers.Main) {
                    val savedLoginList = list.map { it.mapToSavedLogin() }
                    loginsFragmentStore.dispatch(
                        LoginsAction.ListOfDupes(
                            savedLoginList
                        )
                    )
                }
            }
        }
        fetchLoginJob.invokeOnCompletion {
            if (it is CancellationException) {
                deferredLogin?.cancel()
            }
        }
    }

    fun fetchLoginDetails(loginId: String) {
        var deferredLogin: Deferred<List<Login>>? = null
        val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) {
            deferredLogin = async {
                passwordsStorage.list()
            }
            val fetchedLoginList = deferredLogin?.await()

            fetchedLoginList?.let {
                withContext(Dispatchers.Main) {
                    val login = fetchedLoginList.filter {
                        it.guid == loginId
                    }.first()
                    loginsFragmentStore.dispatch(
                        LoginsAction.UpdateCurrentLogin(
                            login.mapToSavedLogin()
                        )
                    )
                }
            }
        }
        fetchLoginJob.invokeOnCompletion {
            if (it is CancellationException) {
                deferredLogin?.cancel()
            }
        }
    }

    fun handleLoadAndMapLogins() {
        var deferredLogins: Deferred<List<Login>>? = null
        val fetchLoginsJob = viewLifecycleScope.launch(ioDispatcher) {
            deferredLogins = async {
                passwordsStorage.list()
            }
            val logins = deferredLogins?.await()
            logins?.let {
                withContext(Dispatchers.Main) {
                    loginsFragmentStore.dispatch(
                        LoginsAction.UpdateLoginsList(
                            logins.map { it.mapToSavedLogin() })
                    )
                }
            }
        }
        fetchLoginsJob.invokeOnCompletion {
            if (it is CancellationException) {
                deferredLogins?.cancel()
            }
        }
    }
}
