package com.kunzisoft.keepass.credentialprovider.passkey.util

import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.provider.CallingAppInfo
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.security.KeyStore
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.Instant
import java.util.UUID
import javax.crypto.KeyGenerator
import javax.crypto.Mac
import javax.crypto.SecretKey

/**
 * Utility class to manage the password and passkey elements,
 * allows to add and retrieve intent values with preconfigured keys,
 * and makes it easy to create creation and usage requests
 */
@RequiresApi(Build.VERSION_CODES.P)
object PassHelper {

    private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
    private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
    private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"

    private const val SEPARATOR = "_"
    private const val HMAC_TYPE = "HmacSHA256"
    private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
    private const val KEYSTORE_TYPE = "AndroidKeyStore"

    private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)

    private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
    private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars

    private const val MAX_DIFF_IN_SECONDS = 60

    private val internalSecureRandom: SecureRandom = SecureRandom()

    /**
     * Add an authentication code generated by an entry to the intent
     */
    fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) {
        putExtras(Bundle().apply {
            val timestamp = Instant.now().epochSecond
            putString(EXTRA_TIMESTAMP, timestamp.toString())
            putString(
                EXTRA_AUTHENTICATION_CODE,
                generatedAuthenticationCode(
                    passkeyEntryNodeId, timestamp
                ).toHexString()
            )
        })
    }

    /**
     * Add the app origin to the intent
     */
    fun Intent.addAppOrigin(appOrigin: AppOrigin?) {
        appOrigin?.let {
            putExtra(EXTRA_APP_ORIGIN, appOrigin)
        }
    }

    /**
     * Retrieve the app origin from the intent
     */
    fun Intent.retrieveAppOrigin(): AppOrigin? {
        return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN)
    }

    /**
     * Remove the app origin from the intent
     */
    fun Intent.removeAppOrigin() {
        return this.removeExtra(EXTRA_APP_ORIGIN)
    }

    /**
     * Check the timestamp and authentication code transmitted via PendingIntent
     */
    fun checkSecurity(intent: Intent, nodeId: UUID?) {
        val timestampString = intent.getStringExtra(EXTRA_TIMESTAMP)
        if (timestampString.isNullOrEmpty())
            throw CreateCredentialUnknownException("Timestamp null")
        if (timestampString.matches(REGEX_TIMESTAMP).not()) {
            throw CreateCredentialUnknownException("Timestamp not valid")
        }
        val timestamp = timestampString.toLong()
        val diff = Instant.now().epochSecond - timestamp
        if (diff !in 0..MAX_DIFF_IN_SECONDS) {
            throw CreateCredentialUnknownException("Out of time")
        }
        verifyAuthenticationCode(
            intent.getStringExtra(EXTRA_AUTHENTICATION_CODE),
            generatedAuthenticationCode(nodeId, timestamp)
        )
    }

    /**
     * Verify the authentication code from the encrypted message received from the intent
     */
    private fun verifyAuthenticationCode(
        valueToCheck: String?,
        authenticationCode: ByteArray
    ) {
        if (valueToCheck.isNullOrEmpty())
            throw CreateCredentialUnknownException("Authentication code empty")
        if (valueToCheck.matches(REGEX_AUTHENTICATION_CODE).not())
            throw CreateCredentialUnknownException("Authentication not valid")

        // Convert the hex string from intent back to bytes and compare
        val intentCode = valueToCheck.chunked(2)
            .map { it.toInt(16).toByte() }
            .toByteArray()
        if (!MessageDigest.isEqual(authenticationCode, intentCode))
            throw CreateCredentialUnknownException("Authentication code incorrect")
    }

    /**
     * Generate the authentication code base on the entry [nodeId] and [timestamp]
     */
    private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
        return generateAuthenticationCode(
            (nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
        )
    }

    /**
     * Generate the authentication code base on the entry [message]
     */
    private fun generateAuthenticationCode(message: String): ByteArray {
        val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
        keyStore.load(null)
        val hmacKey = try {
            keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
        } catch (_: Exception) {
            // key not found
            generateKey()
        }

        val mac = Mac.getInstance(HMAC_TYPE)
        mac.init(hmacKey)
        val authenticationCode = mac.doFinal(message.toByteArray())
        return authenticationCode
    }

    /**
     * Generate the HMAC key if it is not present in the Keystore
     */
    private fun generateKey(): SecretKey? {
        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
        )
        // Recommended for HMAC-256
        val keySizeInBits = 256
        keyGenerator.init(
            KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
                .setKeySize(keySizeInBits)
                .build()
        )
        val key = keyGenerator.generateKey()
        return key
    }

    /**
     * Generate a credential id randomly
     */
    fun generateCredentialId(): ByteArray {
        // see https://w3c.github.io/webauthn/#credential-id
        val size = 16
        val credentialId = ByteArray(size)
        internalSecureRandom.nextBytes(credentialId)
        return credentialId
    }

    /**
     * Utility method to retrieve the origin asynchronously,
     * checks for the presence of the application in the privilege lists
     *
     * @param providedClientDataHash Client data hash precalculated by the system
     * @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
     * @param context Context for file operations.
     * call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
     * call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
     */
    suspend fun getOrigin(
        providedClientDataHash: ByteArray?,
        callingAppInfo: CallingAppInfo?,
        context: Context,
        onOriginRetrieved: suspend (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
        onOriginNotRetrieved: suspend (appOrigin: AppOrigin, androidOriginString: String) -> Unit
    ) {
        if (callingAppInfo == null) {
            throw SecurityException("Calling app info cannot be retrieved")
        }
        withContext(Dispatchers.IO) {

            // For trusted browsers like Chrome and Firefox
            val callOrigin = try {
                PrivilegedAllowLists.getOriginFromPrivilegedAllowLists(callingAppInfo, context)
            } catch (e: Exception) {
                // Throw the Privileged Exception only if it's a browser
                if (e is PrivilegedAllowLists.PrivilegedException
                    && AppUtil.getInstalledBrowsersWithSignatures(context).any {
                        it.packageName == e.temptingApp.packageName
                    }
                ) throw e
                null
            }

            // Build the default Android origin
            val androidOrigin = AndroidOrigin(
                packageName = callingAppInfo.packageName,
                fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
            )

            // Check if the webDomain is validated by the system
            withContext(Dispatchers.Main) {
                if (callOrigin.isNullOrEmpty().not() && providedClientDataHash != null) {
                    // Origin already defined by the system
                    Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
                    onOriginRetrieved(
                        AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
                        providedClientDataHash
                    )
                } else {
                    // Add Android origin by default
                    onOriginNotRetrieved(
                        AppOrigin(verified = false).apply {
                            addAndroidOrigin(androidOrigin)
                        },
                        androidOrigin.toOriginValue()
                    )
                }
            }
        }
    }

    /**
     * Utility method to retrieve the origin asynchronously,
     * checks for the presence of the application in the privilege lists
     *
     * @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
     * @param context Context for file operations.
     * call [onOriginRetrieved] in all cases
     */
    suspend fun getOrigin(
        callingAppInfo: CallingAppInfo?,
        context: Context,
        onOriginRetrieved: suspend (appOrigin: AppOrigin) -> Unit
    ) {
        if (callingAppInfo == null) {
            throw SecurityException("Calling app info cannot be retrieved")
        }
        withContext(Dispatchers.IO) {
            val callOrigin = try {
                PrivilegedAllowLists.getOriginFromPrivilegedAllowLists(callingAppInfo, context)
            } catch (e: Exception) {
                Log.w(PassHelper::class.simpleName, "Unable to retrieve origin from privileged lists", e)
                null
            }
            val androidOrigin = AndroidOrigin(
                packageName = callingAppInfo.packageName,
                fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
            )
            withContext(Dispatchers.Main) {
                if (callOrigin.isNullOrEmpty().not()) {
                    Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
                    onOriginRetrieved(
                        AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
                    )
                } else {
                    onOriginRetrieved(
                        AppOrigin(verified = true).apply {
                            addAndroidOrigin(androidOrigin)
                        }
                    )
                }
            }
        }
    }
}