package dev.privacyllc.period.core.security

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.Mac
import javax.crypto.SecretKey

/**
 * The signing key for the PIN verifier, and the shortest file in this feature
 * for a reason: everything testable lives elsewhere.
 *
 * ## The omissions are the design
 *
 * The builder below sets three things and deliberately omits six. Each omission
 * is what keeps this key **alive** across something the user is entitled to do,
 * and under the no-recovery policy a key that dies takes their entire history
 * with it. Anyone tempted to "harden" this should read the list first:
 *
 * - **`setUserAuthenticationRequired`** — the big one. It would bind this key to
 *   the *device* lock screen, so changing a passcode, or removing the screen
 *   lock, destroys it. It would also be a straightforward bypass: whoever knows
 *   the phone's own PIN would unlock this app with it, making the app lock
 *   exactly as strong as the lock it exists to sit behind. `SECURITY.md` names
 *   "someone who knows the unlock PIN" as an adversary this app cannot stop —
 *   binding to that PIN would hand them the app as well.
 * - **`setUserAuthenticationParameters`** — meaningless without the above.
 * - **`setInvalidatedByBiometricEnrollment`** — a no-op without user
 *   authentication, and named here so nobody adds it thinking it does something.
 *   The *biometric* key in [BiometricKey] does set it, correctly, because
 *   invalidation there degrades to "use your PIN" instead of to lockout.
 * - **`setUnlockedDeviceRequired`** — would fail exactly in the background,
 *   where the reminder worker runs with the screen off.
 * - **`setIsStrongBoxBacked`** — StrongBox defends against extracting the key
 *   from the TEE, which is an adversary `SECURITY.md` explicitly declines to
 *   buy defences for. It throws `StrongBoxUnavailableException` on devices
 *   without it, so it adds a fallback path, and every fallback path is another
 *   way to end up holding a *different* key than the one that made the record.
 * - **`setKeyValidityEnd` / `setMaxUsageCount`** — an expiring key is an
 *   expiring history.
 *
 * ## It is created once and never re-created
 *
 * [ensureKey] is called at enrolment. Nothing calls it to recover from a
 * failure, and nothing may: a fresh key cannot verify an existing record, so
 * minting one inside a `catch` converts a transient Keystore fault into
 * permanent, silent loss. When the key is gone, [hasKey] returns false and the
 * caller takes the `KeyUnavailable` branch, which says so honestly.
 */
internal class AndroidKeyStoreMacProvider(
    private val alias: String = PIN_KEY_ALIAS,
) : MacProvider {

    private fun store(): KeyStore =
        KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }

    override fun hasKey(): Boolean =
        runCatching { store().containsAlias(alias) }.getOrDefault(false)

    override fun ensureKey() {
        if (hasKey()) return
        KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256, ANDROID_KEYSTORE).apply {
            init(
                KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_SIGN)
                    .setKeySize(KEY_BITS)
                    .setDigests(KeyProperties.DIGEST_SHA256)
                    .build(),
            )
        }.generateKey()
    }

    override fun mac(data: ByteArray): ByteArray {
        val key = store().getKey(alias, null) as? SecretKey
            ?: throw IllegalStateException("the verifier key is absent")
        return Mac.getInstance(MAC_ALGORITHM).apply { init(key) }.doFinal(data)
    }

    override fun deleteKey() {
        runCatching { store().deleteEntry(alias) }
    }

    companion object {
        const val ANDROID_KEYSTORE = "AndroidKeyStore"
        const val PIN_KEY_ALIAS = "period.applock.pin.v1"
        const val MAC_ALGORITHM = "HmacSHA256"
        const val KEY_BITS = 256
    }
}
