package nl.whiteboxsystems.tokens

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import nl.aet.cid.client.sdk.SignDataRequest
import nl.aet.cid.client.sdk.SignDataRequestBag
import nl.aet.cid.client.sdk.types.Certificate
import nl.aet.cid.client.sdk.zorgid.constants.HashAlgorithNames
import nl.aet.cid.client.sdk.zorgid.constants.ProfileNames
import java.io.ByteArrayInputStream
import java.net.URL
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.util.*


val PKCS1V15Sha256Prefix = byteArrayOfInts(0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20)

object KDateTime : KSerializer<Calendar?> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)

    override fun serialize(encoder: Encoder, value: Calendar?) {
        if (value != null) {
            encoder.encodeLong(value.timeInMillis / 1000)
        }
    }

    override fun deserialize(decoder: Decoder): Calendar? {
        val timestamp = decoder.decodeLong()
        val c = Calendar.getInstance()
        c.timeInMillis = timestamp * 1000
        return c
    }
}

@Serializable
data class User(val uzi_id: String, val uzi_ura: String) {}

@Serializable
data class Header(val x5c: List<String>, val alg : String = "") {
    fun Json() : String {
        return Json.encodeToString(this)
    }

    fun Compact() : String {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(Json().toByteArray())
    }
}

@Serializable
data class System(val key_fp: String) {}

@Serializable
data class Payload(val challenge: String, val authorizationSubject: String, val url : String, val user: User, val system: System) {
    @Serializable(with = KDateTime::class)
    var timestamp : Calendar? = null

    fun Json() : String {
        return Json.encodeToString(this)
    }

    fun Compact() : String {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(Json().toByteArray())
    }
}


fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }

fun extractCertFP(cert: Certificate): String {
    val message: ByteArray = Base64.getDecoder().decode(cert.rawValue)
    val certFactory: CertificateFactory = CertificateFactory.getInstance("X509")
    val inputStream = ByteArrayInputStream(message)
    val _cert: java.security.cert.Certificate? = certFactory.generateCertificate(inputStream)
    val md = MessageDigest.getInstance("SHA-256")
    val digest: ByteArray = md.digest(_cert?.publicKey?.encoded)
    val fp = "sha256:${digest.toHex()}"
    println("fp: ${fp}")
    return fp
}

fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }

class WBXAuthToken(challenge: String, bsn: String, url :String, cert: Certificate?, system_key_fp: String?) {
    private var header = cert?.let { Header(listOf(it.rawValue), "RS256") }
    private var payload = cert?.parsedFields?.get("UziNumber")?.let { cert?.parsedFields?.get("UziRegisterSubscriberNumber")?.let { it1 -> User(it, it1) } }
        ?.let { Payload(challenge, bsn, url, it, system_key_fp?.let { fp -> System(fp) } ?: System(extractCertFP(cert))) }
    private var signature = ""

    init {
        updateTimestamp()
    }

    private fun getSignData() : String {
        return  "${header?.Compact()}.${payload?.Compact()}"
    }

    private fun getSignDataHashed() : String {
        val signData = getSignData()
        val digest = MessageDigest.getInstance("SHA-256")
        val encodedHash = digest.digest(
            signData.toByteArray()
        )

        // The ZorgID SDK signs without applying EMSA-PKCS1-v1_5 encoding. To accommodate
        // for this, prepend the sha256 OID to the signed data so that it complies to the JWS
        // RS256 algorithm.
        // For more info: https://datatracker.ietf.org/doc/html/rfc3447#section-9.2
        return  Base64.getEncoder().encodeToString(PKCS1V15Sha256Prefix + encodedHash)
    }

    // updateTimestamp set the token's timestamp to now. updateTimestamp is called
    // when de token is instantiated. It can be called on a later moment to update the timestamp.
    // This can be useful if there is much time between the instantiation and actual signing
    // of the token. Note: don't update timestamp after calling the getHashBag method.
    // The timestamp is part of the signed data so calling teh update timestamp method after signing
    // the data results in an invalid JWS
    fun updateTimestamp() {
        payload?.timestamp =  Calendar.getInstance()
    }

    // getHashBag returns the data that needs to be signed, wrapped in a SignDataRequestBag.
    // The return value of this method can be used directly to call the ITransactionManager.sign method.
    fun getHashBag() : SignDataRequestBag {
        val hashBag = SignDataRequestBag(ProfileNames.Hash1, HashAlgorithNames.Sha256)
        hashBag.add(SignDataRequest(getSignDataHashed()))

        return hashBag
    }

    // convertAndSetSignature converts the signature returned by the ZorgID SDK. This signature is in
    // base64 std encoding. JWS's need to have base64 URL encoding without padding.
    fun convertAndSetSignature(sig : String) {
        signature = Base64.getUrlEncoder().withoutPadding().encodeToString(Base64.getDecoder().decode(sig))
    }

    // jws returns the WBXToken in jws compact format
    fun jws() : String {
        return this.getSignData() + "." + signature
    }

    // url returns the Whitebox url for which this token provides access
    fun url() : String {
        if (payload == null) {
            return ""
        }
        return payload!!.url
    }

    // endpoint returns the endpoint to which the JWS and URL need to be posted
    fun endpoint() : String {
        if (payload == null) {
            return ""
        }
        var parsed = URL(payload?.url)
        var startURL = "${parsed.protocol}://${parsed.host}"

        if (parsed.port > 0 && parsed.port != 443) {
            startURL += ":${parsed.port}"
        }

        startURL += "/topicus"

        return startURL
    }
}