package com.hegocre.nextcloudpasswords.api.encryption

import com.goterl.lazysodium.interfaces.Box
import com.goterl.lazysodium.interfaces.GenericHash
import com.goterl.lazysodium.interfaces.PwHash
import com.hegocre.nextcloudpasswords.BuildConfig
import com.hegocre.nextcloudpasswords.api.exceptions.PWDv1ChallengeMasterKeyNeededException
import com.hegocre.nextcloudpasswords.api.exceptions.PWDv1ChallengePasswordException
import com.hegocre.nextcloudpasswords.api.exceptions.SodiumDecryptionException
import com.hegocre.nextcloudpasswords.utils.Error
import com.hegocre.nextcloudpasswords.utils.LazySodiumUtils
import com.hegocre.nextcloudpasswords.utils.Result
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

/**
 * A PWDv1Challenge object. See the
 * [API reference](https://git.mdns.eu/nextcloud/passwords/-/wikis/Developers/Encryption/PWDv1Challenge).
 *
 * @property salts An array with the salts to compute the secret.
 * @property secret An encoded string with the secret, only present when creating a challenge.
 */
data class PWDv1Challenge(
    val salts: Array<String>,
    val secret: String? = null
) {
    /**
     * Solve the PWDv1 challenge according to the API reference.
     *
     * @param password The password used to solve the challenge.
     * @return A result with the solved challenge if success, and an error code otherwise.
     */
    fun solve(password: String?): Result<String> {
        // Challenge is empty
        if (salts.size != 3) return Result.Error(Error.API_NO_CSE)

        // Master key needed but not provided
        if (password == null) throw PWDv1ChallengeMasterKeyNeededException()

        // TODO: Warn the user
        if (password.length < 12) throw PWDv1ChallengePasswordException("Password should have no less than 12 characters")
        if (password.length > 128) throw PWDv1ChallengePasswordException("Password should have no more than 128 characters")

        val sodium = LazySodiumUtils.getSodium()

        val passwordSalt = sodium.sodiumHex2Bin(salts[0])
        val genericHashKey = sodium.sodiumHex2Bin(salts[1])
        val passwordHashSalt = sodium.sodiumHex2Bin(salts[2])
        val input = sodium.bytes(password) + passwordSalt

        val genericHash = ByteArray(GenericHash.BYTES_MAX)
        if (!sodium.cryptoGenericHash(
                genericHash,
                genericHash.size,
                input,
                input.size.toLong(),
                genericHashKey,
                genericHashKey.size
            )
        ) throw SodiumDecryptionException("Could not create generic hash")

        val passwordHash = ByteArray(Box.SEEDBYTES)
        if (!sodium.cryptoPwHash(
                passwordHash,
                passwordHash.size,
                genericHash,
                genericHash.size,
                passwordHashSalt,
                PwHash.OPSLIMIT_INTERACTIVE,
                PwHash.MEMLIMIT_INTERACTIVE,
                PwHash.Alg.PWHASH_ALG_ARGON2ID13
            )
        ) throw SodiumDecryptionException("Could not create password hash")

        return Result.Success(sodium.sodiumBin2Hex(passwordHash).lowercase())
    }

    /* Generated by Android Studio */
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as PWDv1Challenge

        return salts.contentEquals(other.salts)
    }

    /* Generated by Android Studio */
    override fun hashCode(): Int {
        return salts.contentHashCode()
    }

    companion object {
        /**
         * Creates a [PWDv1Challenge] from a JSON object.
         *
         * @param data The data as a JSON object.
         * @return The challenge created from the JSON.
         */
        fun fromJson(data: String): PWDv1Challenge {
            val obj = JSONObject(data)

            val saltsArray = try {
                val challengeObj = obj.getJSONObject("challenge")
                challengeObj.getJSONArray("salts")
            } catch (e: JSONException) {
                if (BuildConfig.DEBUG) {
                    e.printStackTrace()
                }
                JSONArray()
            }

            val salts = Array(saltsArray.length()) { "" }

            for (i in 0 until saltsArray.length()) {
                salts[i] = saltsArray.getString(i)
            }

            return PWDv1Challenge(salts)
        }
    }
}