package dev.privacyllc.period.feature.lock

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.privacyllc.period.core.datastore.UserPreferencesRepository
import dev.privacyllc.period.core.security.AppLockRepository
import dev.privacyllc.period.core.security.UnlockResult
import dev.privacyllc.period.lock.AppLockController
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

/** What the App lock settings screen is showing. */
data class LockSettings(
    val hasPin: Boolean = false,
    val biometricEnabled: Boolean = false,
    val busy: Boolean = false,
    val message: Message? = null,
) {
    enum class Message { PIN_SET, PIN_REMOVED, WRONG_PIN, COULD_NOT_SET }
}

/**
 * Turning the lock on and off, and changing the PIN.
 *
 * ## Removing the lock asks for the PIN first
 *
 * Otherwise the lock protects nothing: anybody who picks up an unlocked phone
 * could open Settings and switch it off, which is precisely the adversary
 * `SECURITY.md` names first. Setting a PIN for the first time needs no
 * authentication, because there is nothing yet to authenticate against.
 */
@HiltViewModel
class LockSettingsViewModel @Inject constructor(
    private val lock: AppLockRepository,
    private val preferences: UserPreferencesRepository,
    private val controller: AppLockController,
) : ViewModel() {

    private val _busy = MutableStateFlow(false)
    private val _message = MutableStateFlow<LockSettings.Message?>(null)

    val state: StateFlow<LockSettings> = combine(
        lock.hasPin,
        preferences.preferences.map { it.biometricLockEnabled },
        _busy,
        _message,
    ) { hasPin, biometric, busy, message ->
        LockSettings(
            hasPin = hasPin,
            // Meaningless without a PIN to stand in for, so never shown as on
            // without one. The stored flag is left alone rather than corrected,
            // so turning the lock back on restores the choice the user made.
            biometricEnabled = biometric && hasPin,
            busy = busy,
            message = message,
        )
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), LockSettings())

    private val handler = CoroutineExceptionHandler { _, _ ->
        _busy.value = false
        _message.value = LockSettings.Message.COULD_NOT_SET
    }

    /** First PIN, or a replacement once [verifyCurrent] has passed. */
    fun setPin(pin: CharArray) {
        if (_busy.value) return
        _busy.value = true
        viewModelScope.launch(handler) {
            val ok = lock.setPin(pin)
            pin.fill('\u0000')
            // Stay unlocked in the session that just set the PIN. Without this
            // the gate flips shut the instant `hasPin` turns true, and the user
            // is asked for a PIN one second after choosing it — which reads as
            // the app not having understood, and is the first thing they see of
            // a feature they have just been warned is unrecoverable.
            if (ok) controller.unlock()
            _busy.value = false
            _message.value =
                if (ok) LockSettings.Message.PIN_SET else LockSettings.Message.COULD_NOT_SET
        }
    }

    /**
     * Check the current PIN, then run [onVerified].
     *
     * Used by both "turn the lock off" and "change PIN". A wrong answer here
     * goes through the same backoff as the lock screen, because otherwise
     * Settings would be an unlimited guessing oracle for the same secret.
     */
    fun verifyCurrent(pin: CharArray, onVerified: suspend () -> Unit) {
        if (_busy.value) return
        _busy.value = true
        viewModelScope.launch(handler) {
            val outcome = lock.check(pin)
            pin.fill('\u0000')
            if (outcome is UnlockResult.Unlocked) {
                onVerified()
                _busy.value = false
            } else {
                _busy.value = false
                _message.value = LockSettings.Message.WRONG_PIN
            }
        }
    }

    fun removePin(current: CharArray) = verifyCurrent(current) {
        lock.clearLock()
        preferences.setBiometricLockEnabled(false)
        _message.value = LockSettings.Message.PIN_REMOVED
    }

    fun setBiometricEnabled(enabled: Boolean) {
        viewModelScope.launch(handler) { preferences.setBiometricLockEnabled(enabled) }
    }

    fun clearMessage() {
        _message.value = null
    }
}
