package com.tt.honeyDue.security

import androidx.biometric.BiometricManager as AndroidXBiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

/**
 * P6 Stream T — Android biometric authentication wrapper.
 *
 * Thin layer over [androidx.biometric.BiometricPrompt] that exposes a
 * suspend function returning a typed [Result] — parity with iOS
 * LAContext-based unlock in the SwiftUI app.
 *
 * Features:
 *  - 3-strike lockout: after [MAX_FAILURES] consecutive failures in a row,
 *    the next [authenticate] call returns [Result.TooManyAttempts] WITHOUT
 *    showing the system prompt. The caller must drive the fallback (PIN)
 *    flow and reset the counter via [reset].
 *  - NO_HARDWARE bypass: [canAuthenticate] surfaces whether a prompt can
 *    even be shown, so callers can skip the lock screen entirely on
 *    devices without biometric hardware.
 *
 * The real [BiometricPrompt] is obtained via [promptFactory] so unit tests
 * can inject a fake that directly invokes the callback. In production the
 * default factory wires the activity + main-thread executor.
 */
class BiometricManager(
    private val activity: FragmentActivity,
    private val promptFactory: (BiometricPrompt.AuthenticationCallback) -> Prompter =
        { callback ->
            val executor = ContextCompat.getMainExecutor(activity)
            val prompt = BiometricPrompt(activity, executor, callback)
            Prompter { info -> prompt.authenticate(info) }
        },
    private val availabilityProbe: () -> Availability = {
        defaultAvailability(activity)
    },
) {

    /** Allows tests to intercept the [BiometricPrompt.authenticate] call. */
    fun interface Prompter {
        fun show(info: BiometricPrompt.PromptInfo)
    }

    /** High-level outcome returned by [authenticate]. */
    sealed class Result {
        object Success : Result()
        object UserCanceled : Result()
        /** 3+ consecutive failures — caller must switch to PIN fallback. */
        object TooManyAttempts : Result()
        /** Device lacks biometric hardware or enrollment — bypass lock. */
        object NoHardware : Result()
        data class Error(val code: Int, val message: String) : Result()
    }

    /** Result of [canAuthenticate]; drives whether to show the lock screen. */
    enum class Availability {
        NO_HARDWARE,
        NOT_ENROLLED,
        AVAILABLE,
    }

    private var consecutiveFailures: Int = 0

    /** Quick probe — does this device support biometric auth right now? */
    fun canAuthenticate(): Availability = availabilityProbe()

    /** Resets the 3-strike counter — call after a successful fallback. */
    fun reset() {
        consecutiveFailures = 0
    }

    /**
     * Show the biometric prompt and suspend until the user resolves it.
     *
     * Returns [Result.TooManyAttempts] immediately (without showing a
     * prompt) when the 3-strike threshold has been crossed.
     */
    suspend fun authenticate(
        title: String,
        subtitle: String? = null,
        negativeButtonText: String = "Cancel",
    ): Result {
        if (consecutiveFailures >= MAX_FAILURES) {
            return Result.TooManyAttempts
        }

        return suspendCancellableCoroutine { cont ->
            val callback = object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(
                    result: BiometricPrompt.AuthenticationResult
                ) {
                    consecutiveFailures = 0
                    if (cont.isActive) cont.resume(Result.Success)
                }

                override fun onAuthenticationFailed() {
                    // Per Android docs, this does NOT dismiss the prompt —
                    // the system continues listening. We count the strike
                    // but do NOT resume here; resolution comes via
                    // onAuthenticationError or onAuthenticationSucceeded.
                    consecutiveFailures++
                }

                override fun onAuthenticationError(
                    errorCode: Int,
                    errString: CharSequence,
                ) {
                    if (!cont.isActive) return
                    val result = when (errorCode) {
                        BiometricPrompt.ERROR_USER_CANCELED,
                        BiometricPrompt.ERROR_NEGATIVE_BUTTON,
                        BiometricPrompt.ERROR_CANCELED -> Result.UserCanceled

                        BiometricPrompt.ERROR_HW_NOT_PRESENT,
                        BiometricPrompt.ERROR_HW_UNAVAILABLE,
                        BiometricPrompt.ERROR_NO_BIOMETRICS -> Result.NoHardware

                        BiometricPrompt.ERROR_LOCKOUT,
                        BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> {
                            consecutiveFailures = MAX_FAILURES
                            Result.TooManyAttempts
                        }

                        else -> Result.Error(errorCode, errString.toString())
                    }
                    cont.resume(result)
                }
            }

            val info = BiometricPrompt.PromptInfo.Builder()
                .setTitle(title)
                .apply { subtitle?.let(::setSubtitle) }
                .setNegativeButtonText(negativeButtonText)
                .setAllowedAuthenticators(
                    AndroidXBiometricManager.Authenticators.BIOMETRIC_STRONG or
                        AndroidXBiometricManager.Authenticators.BIOMETRIC_WEAK
                )
                .build()

            promptFactory(callback).show(info)
        }
    }

    /** Test hook — inspect current strike count. */
    internal fun currentFailureCount(): Int = consecutiveFailures

    /** Test hook — seed the strike count (e.g. to simulate prior failures). */
    internal fun seedFailures(count: Int) {
        consecutiveFailures = count
    }

    companion object {
        /** Matches iOS LAContext 3-strike convention. */
        const val MAX_FAILURES: Int = 3

        private fun defaultAvailability(activity: FragmentActivity): Availability {
            val mgr = AndroidXBiometricManager.from(activity)
            return when (
                mgr.canAuthenticate(
                    AndroidXBiometricManager.Authenticators.BIOMETRIC_STRONG or
                        AndroidXBiometricManager.Authenticators.BIOMETRIC_WEAK
                )
            ) {
                AndroidXBiometricManager.BIOMETRIC_SUCCESS -> Availability.AVAILABLE
                AndroidXBiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> Availability.NOT_ENROLLED
                AndroidXBiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
                AndroidXBiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE,
                AndroidXBiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> Availability.NO_HARDWARE
                else -> Availability.NO_HARDWARE
            }
        }
    }
}
