package no.zyon.shade.keystore

import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import javax.crypto.Cipher
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine

/**
 * Biometric unlock for a `KeystoreMasterKey`-bound `Cipher`.
 *
 * The Android keystore enforces that any operation on a
 * user-authentication-required key must happen via a
 * `BiometricPrompt.CryptoObject`-wrapped `Cipher`. The user sees a
 * system biometric prompt; on success the same `Cipher` instance is
 * usable for one operation (or one streaming session) before
 * needing to re-prompt.
 *
 * This is a thin coroutine wrapper around `BiometricPrompt` that
 * resolves to the authenticated cipher or throws on user
 * cancellation. Callers typically run it once at app start to
 * unlock the master key for the lifetime of the foreground session.
 */
class BiometricUnlock(
    private val activity: FragmentActivity,
    private val title: String,
    private val subtitle: String? = null,
    private val negativeButton: String = "Cancel",
) {

    /**
     * True if BIOMETRIC_STRONG is currently usable on this device.
     * False means the user has no enrolled fingerprint/face that
     * meets the class-3 assurance level — fall back to a credential
     * recovery flow rather than crashing.
     */
    fun canAuthenticate(): Boolean {
        val mgr = BiometricManager.from(activity)
        return mgr.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
            BiometricManager.BIOMETRIC_SUCCESS
    }

    /**
     * Show the biometric prompt and return the authenticated cipher.
     *
     * Cancellation paths:
     *  - User taps the negative button → throws `BiometricCancelledException`.
     *  - System errors out (e.g. too many failures) → throws
     *    `BiometricFailedException` with the system error code.
     */
    suspend fun unlock(cipher: Cipher): Cipher = suspendCancellableCoroutine { cont ->
        val callback = object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                val authedCipher = result.cryptoObject?.cipher
                if (authedCipher == null) {
                    cont.resumeWithException(
                        BiometricFailedException(-1, "BiometricPrompt returned no cipher"),
                    )
                } else {
                    cont.resume(authedCipher)
                }
            }

            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
                    errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
                    errorCode == BiometricPrompt.ERROR_CANCELED
                ) {
                    cont.resumeWithException(BiometricCancelledException(errString.toString()))
                } else {
                    cont.resumeWithException(
                        BiometricFailedException(errorCode, errString.toString()),
                    )
                }
            }

            override fun onAuthenticationFailed() {
                // A single failed attempt — the prompt stays open and
                // gives the user another try. Don't resume the
                // continuation; let the system flow continue.
            }
        }

        val prompt = BiometricPrompt(
            activity,
            activity.mainExecutor,
            callback,
        )
        val info = BiometricPrompt.PromptInfo.Builder()
            .setTitle(title)
            .apply { if (subtitle != null) setSubtitle(subtitle) }
            .setNegativeButtonText(negativeButton)
            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
            .build()

        cont.invokeOnCancellation { prompt.cancelAuthentication() }
        prompt.authenticate(info, BiometricPrompt.CryptoObject(cipher))
    }
}

/** User cancelled the biometric prompt. */
class BiometricCancelledException(message: String) : RuntimeException(message)

/**
 * BiometricPrompt returned a non-cancellation error (lockout, hardware
 * unavailable, no enrolled biometrics, etc.). Inspect `errorCode`
 * against `BiometricPrompt.ERROR_*` constants to decide UX response.
 */
class BiometricFailedException(val errorCode: Int, message: String) :
    RuntimeException("[$errorCode] $message")
