package org.openrs2.crypto

import jdk.security.jarsigner.JarSigner
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
import org.bouncycastle.asn1.x500.X500NameBuilder
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder
import org.bouncycastle.util.BigIntegers
import org.openrs2.util.io.useAtomicOutputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyFactory
import java.security.KeyStore
import java.time.OffsetDateTime
import java.time.Period
import java.time.ZoneOffset
import java.util.Date
import java.util.jar.JarFile

public class Pkcs12KeyStore private constructor(privateKeyEntry: KeyStore.PrivateKeyEntry, signerName: String) {
    private val signer = JarSigner.Builder(privateKeyEntry)
        .signatureAlgorithm("SHA256withRSA")
        .digestAlgorithm("SHA-256")
        .signerName(signerName)
        .build()

    public fun signJar(input: Path, output: OutputStream) {
        JarFile(input.toFile()).use { file ->
            signer.sign(file, output)
        }
    }

    public companion object {
        private const val ALIAS = "openrs2"

        private const val PASSWORD = ALIAS
        private val PASSWORD_CHARS = PASSWORD.toCharArray()
        private val PASSWORD_PARAMETER = KeyStore.PasswordProtection(PASSWORD_CHARS)

        private const val SERIAL_LENGTH = 128

        private val MAX_CLOCK_SKEW = Period.ofDays(1)
        private val VALIDITY_PERIOD = Period.ofYears(10)

        private val SHA256_WITH_RSA = AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption)
        private val SHA256 = AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256)

        @JvmStatic
        public fun open(path: Path, signerName: String): Pkcs12KeyStore {
            val keyStore = KeyStore.getInstance("PKCS12")
            if (Files.exists(path)) {
                Files.newInputStream(path).use { input ->
                    keyStore.load(input, PASSWORD_CHARS)
                }
            } else {
                keyStore.load(null)
            }

            val privateKeyEntry = if (keyStore.containsAlias(ALIAS)) {
                keyStore.getEntry(ALIAS, PASSWORD_PARAMETER) as KeyStore.PrivateKeyEntry
            } else {
                val entry = createPrivateKeyEntry(signerName)
                keyStore.setEntry(ALIAS, entry, PASSWORD_PARAMETER)

                path.useAtomicOutputStream { output ->
                    keyStore.store(output, PASSWORD_CHARS)
                }

                entry
            }

            return Pkcs12KeyStore(privateKeyEntry, signerName)
        }

        private fun createPrivateKeyEntry(signerName: String): KeyStore.PrivateKeyEntry {
            val (public, private) = Rsa.generateKeyPair(Rsa.JAR_KEY_LENGTH)

            val dname = X500NameBuilder()
                .addRDN(BCStyle.CN, signerName)
                .build()

            val serial = BigIntegers.createRandomBigInteger(SERIAL_LENGTH, secureRandom)

            val start = OffsetDateTime.now(ZoneOffset.UTC).minus(MAX_CLOCK_SKEW)
            val end = start.plus(VALIDITY_PERIOD)

            val spki = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(public)
            val signer = BcRSAContentSignerBuilder(SHA256_WITH_RSA, SHA256).build(private)

            val certificate = X509v3CertificateBuilder(
                dname,
                serial,
                Date.from(start.toInstant()),
                Date.from(end.toInstant()),
                dname,
                spki
            ).build(signer)

            val jcaPrivate = KeyFactory.getInstance("RSA").generatePrivate(private.toKeySpec())
            val jcaCertificate = JcaX509CertificateConverter().getCertificate(certificate)
            return KeyStore.PrivateKeyEntry(jcaPrivate, arrayOf(jcaCertificate))
        }
    }
}
