/*
 * Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
 * Project: Gajumaru Core Java Libraries <gajumaru.io>
 *
 * This program is dual-licensed:
 * 1) Under the GNU Affero General Public License as published by the Free
 *    Software Foundation, either version 3 of the License, or (at your option)
 *    any later version (AGPL-3.0-or-later).
 *
 * 2) Under a commercial/proprietary license available directly from QPQ IaaS AG.
 *    If you wish to use this software outside the strict constraints of the
 *    AGPLv3 (e.g. within a closed-source or proprietary product), you must
 *    purchase a commercial license from QPQ IaaS AG.
 *
 * Authors:
 *   - Craig Everett <craigeverett@qpq.swiss>
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
 */

package swiss.qpq.gajumobile.security

import android.content.Context
import kotlinx.serialization.Serializable
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import swiss.qpq.gajumaru.core.tools.CryptoUtils

@Serializable
data class EncryptedEnvelope(
    val encryptedData: ByteArray,
    val encryptedDek: ByteArray,
    val dataIv: ByteArray,
    val dekIv: ByteArray,
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as EncryptedEnvelope

        if (!encryptedData.contentEquals(other.encryptedData)) return false
        if (!encryptedDek.contentEquals(other.encryptedDek)) return false
        if (!dataIv.contentEquals(other.dataIv)) return false
        return dekIv.contentEquals(other.dekIv)
    }

    override fun hashCode(): Int {
        var result = encryptedData.contentHashCode()
        result = (31 * result) + encryptedDek.contentHashCode()
        result = (31 * result) + dataIv.contentHashCode()
        result = (31 * result) + dekIv.contentHashCode()
        return result
    }
}

object EncryptionService {
    private const val AES_GCM = "AES/GCM/NoPadding"
    private const val GCM_TAG_LENGTH = 128
    private const val IV_LENGTH = 12

    @JvmStatic
    fun encrypt(context: Context, data: ByteArray): EncryptedEnvelope {
        val masterKey = KeyManager.getMasterKey(context)
        return encrypt(masterKey, data)
    }

    @JvmStatic
    fun encrypt(masterKey: SecretKey, data: ByteArray): EncryptedEnvelope {
        // 1. Generate a random Data Encryption Key (DEK) in software
        val dek = generateDek()
        val dekBytes = dek.encoded

        try {
            // 2. Encrypt the data with the DEK
            val (encryptedData, dataIv) = encryptRaw(dek, data)

            // 3. Encrypt the DEK with the Master Key (Hardware-backed)
            val (encryptedDek, dekIv) = encryptRaw(masterKey, dekBytes)

            return EncryptedEnvelope(
                encryptedData = encryptedData,
                encryptedDek = encryptedDek,
                dataIv = dataIv,
                dekIv = dekIv
            )
        } finally {
            CryptoUtils.wipe(dekBytes)
        }
    }

    @JvmStatic
    fun decrypt(context: Context, envelope: EncryptedEnvelope): ByteArray {
        val masterKey = KeyManager.getMasterKey(context)
        return decrypt(masterKey, envelope)
    }

    @JvmStatic
    fun decrypt(masterKey: SecretKey, envelope: EncryptedEnvelope): ByteArray {
        var decryptedDekBytes: ByteArray? = null
        try {
            // 1. Decrypt the DEK using the Master Key
            decryptedDekBytes = decryptRaw(masterKey, envelope.dekIv, envelope.encryptedDek)
            val dek = SecretKeySpec(decryptedDekBytes, "AES")

            // 2. Decrypt the data using the DEK
            return decryptRaw(dek, envelope.dataIv, envelope.encryptedData)
        } catch (e: Exception) {
            val msg = e.message ?: e.toString()
            throw Exception("EncryptionService.decrypt failed: $msg", e)
        } finally {
            if (decryptedDekBytes != null) CryptoUtils.wipe(decryptedDekBytes)
        }
    }

    @JvmStatic
    fun encryptRaw(key: SecretKey, data: ByteArray): Pair<ByteArray, ByteArray> {
        try {
            val cipher = Cipher.getInstance(AES_GCM)
            
            // Try initializing without a specific IV to allow the provider (e.g. AndroidKeyStore)
            // to generate a high-entropy randomized IV.
            var iv: ByteArray?
            try {
                // We still want to specify the tag length if possible.
                // However, many providers will default to 128 bits.
                cipher.init(Cipher.ENCRYPT_MODE, key)
                iv = cipher.iv
            } catch (e: Exception) {
                // Fallback for software providers that require explicit IV or GCM specs during init
                val manualIv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) }
                cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LENGTH, manualIv))
                iv = manualIv
            }

            if (iv == null || iv.isEmpty()) {
                // Some providers might not generate it until doFinal or at all if misconfigured
                val manualIv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) }
                cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LENGTH, manualIv))
                iv = manualIv
            }

            val ciphertext = cipher.doFinal(data)
            return ciphertext to iv!!
        } catch (e: Exception) {
            throw Exception("encryptRaw failed: ${e.message}", e)
        }
    }

    @JvmStatic
    fun decryptRaw(key: SecretKey, iv: ByteArray, encryptedData: ByteArray): ByteArray {
        try {
            if (iv.size != IV_LENGTH && iv.size != 16) {
                android.util.Log.w("EncryptionService", "Unexpected IV length: ${iv.size}. Expected 12 or 16.")
            }
            val cipher = Cipher.getInstance(AES_GCM)
            cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LENGTH, iv))
            return cipher.doFinal(encryptedData)
        } catch (e: Exception) {
            val msg = e.message ?: e.toString()
            throw Exception("decryptRaw failed [${e.javaClass.simpleName}]: $msg", e)
        }
    }

    @JvmStatic
    fun generateDek(): SecretKey {
        val keyGen = KeyGenerator.getInstance("AES")
        keyGen.init(256)
        return keyGen.generateKey()
    }
}
