package dev.privacyllc.period.lock

import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity

/** What the biometric affordance should do, decided once per composition. */
internal class BiometricUnlock(
    val available: Boolean,
    val prompt: () -> Unit,
)

/**
 * The fingerprint path, and the three decisions that shape it.
 *
 * ## `BIOMETRIC_STRONG` only — never `DEVICE_CREDENTIAL`
 *
 * Allowing the device credential would let the phone's own PIN open this app,
 * and three separate reasons rule it out:
 *
 * 1. The owner ruled out recovery paths, and the device PIN is one.
 * 2. In this app's threat model a partner very often knows the phone's PIN.
 *    Accepting it would silently make the app lock exactly as strong as the lock
 *    it sits behind — which is to say, no stronger at all.
 * 3. `PromptInfo.Builder.build()` **throws** for `BIOMETRIC_STRONG or
 *    DEVICE_CREDENTIAL` on API 28 and 29, and for `DEVICE_CREDENTIAL` alone
 *    below 30. That is a crash, not an error callback, on a range this app
 *    supports.
 *
 * `BIOMETRIC_STRONG` alone behaves identically from API 26 to 36, with no
 * version branching.
 *
 * ## Cancels are not failures
 *
 * `ERROR_NEGATIVE_BUTTON` is the user tapping "Use PIN" — an ordinary path with
 * no error copy at all. `ERROR_USER_CANCELED` is a dismissal, and
 * `ERROR_CANCELED` is the system pre-empting the prompt with no user action.
 * None of the three may touch the PIN failure counter, or a stray dismissal
 * would cost somebody a lockout they did not earn.
 *
 * ## Nothing here logs
 *
 * `app` is in `modulesSeeingHealthData`, and `printStackTrace` is in the
 * forbidden list by name. This is exactly where the temptation is highest.
 */
@Composable
internal fun rememberBiometricUnlock(
    activity: FragmentActivity?,
    onBeginAuth: () -> Unit,
    onEndAuth: () -> Unit,
    onUnlocked: () -> Unit,
    onUnavailable: () -> Unit,
): BiometricUnlock {
    // Suppressed for the life of the process once the hardware reports a
    // permanent lockout: only the device credential clears that, and this app's
    // PIN is not it. Re-probed on the next cold start.
    val permanentlyLockedOut = remember { mutableStateOf(false) }

    if (activity == null) return BiometricUnlock(available = false, prompt = {})

    val status = BiometricManager.from(activity)
        .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)

    val available = !permanentlyLockedOut.value && when (status) {
        BiometricManager.BIOMETRIC_SUCCESS -> true
        // "Try it and be ready to fall back" — not "unavailable". The status is
        // genuinely unknown on some devices, and hiding the button there would
        // remove a working unlock.
        BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> true
        else -> false
    }

    val prompt = {
        onBeginAuth()
        val callback = object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                onEndAuth()
                onUnlocked()
            }

            override fun onAuthenticationError(code: Int, message: CharSequence) {
                onEndAuth()
                when (code) {
                    // Ordinary exits. Say nothing, change nothing.
                    BiometricPrompt.ERROR_NEGATIVE_BUTTON,
                    BiometricPrompt.ERROR_USER_CANCELED,
                    BiometricPrompt.ERROR_CANCELED,
                    -> Unit

                    // No fingerprint enrolled any more, or the hardware is gone.
                    // Turn the offer off so the user is not sent back to a
                    // button that cannot work.
                    BiometricPrompt.ERROR_NO_BIOMETRICS,
                    BiometricPrompt.ERROR_HW_NOT_PRESENT,
                    -> onUnavailable()

                    // Cleared only by the device credential, which this app's
                    // PIN is not — so retrying produces the same error forever.
                    BiometricPrompt.ERROR_LOCKOUT_PERMANENT ->
                        permanentlyLockedOut.value = true

                    // Transient: temporary lockout, hardware busy, a vendor
                    // string. Leave the setting alone; the PIN still works.
                    else -> Unit
                }
            }

            override fun onAuthenticationFailed() {
                // A finger that did not match. The prompt stays up and handles
                // its own retries; this must not touch the PIN counter.
            }
        }

        BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), callback)
            .authenticate(
                BiometricPrompt.PromptInfo.Builder()
                    .setTitle("Unlock")
                    .setSubtitle("Use your fingerprint to open the app")
                    .setNegativeButtonText("Use PIN")
                    .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
                    .setConfirmationRequired(false)
                    .build(),
            )
    }

    return BiometricUnlock(available = available, prompt = prompt)
}
