/*
 * Copyright 2004 - 2013 Wayne Grant
 *           2013 - 2025 Kai Kramer
 *
 * This file is part of KeyStore Explorer.
 *
 * KeyStore Explorer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * KeyStore Explorer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with KeyStore Explorer.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kse.crypto.x509;

import java.io.IOException;
import java.math.BigInteger;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.cert.X509Extension;
import java.util.Date;
import java.util.ResourceBundle;
import java.util.concurrent.TimeUnit;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.kse.KSE;
import org.kse.crypto.CryptoException;
import org.kse.crypto.signing.SignatureType;

/**
 * X.509 certificate generator.
 */
public class X509CertificateGenerator {
    private static ResourceBundle res = ResourceBundle.getBundle("org/kse/crypto/x509/resources");
    private X509CertificateVersion version;

    /**
     * Construct the generator.
     *
     * @param version Version of generated certificates
     */
    public X509CertificateGenerator(X509CertificateVersion version) {
        this.version = version;
    }

    /**
     * Generate a certificate.
     *
     * @param subject       Certificate subject
     * @param issuer        Certificate issuer
     * @param validityStart Validity start date of certificate in msecs
     * @param validityEnd   Validity end date of certificate in msecs
     * @param publicKey     Public part of key pair
     * @param privateKey    Private part of key pair
     * @param signatureType Signature Type
     * @param serialNumber  Serial number
     * @return The generated certificate
     * @throws CryptoException If there was a problem generating the certificate
     */
    public X509Certificate generate(X500Name subject, X500Name issuer, Date validityStart, Date validityEnd,
                                    PublicKey publicKey, PrivateKey privateKey, SignatureType signatureType,
                                    BigInteger serialNumber) throws CryptoException {
        return generate(subject, issuer, validityStart, validityEnd, publicKey, privateKey, signatureType, serialNumber,
                        null, KSE.BC);
    }

    /**
     * Generate a certificate.
     *
     * @param subject       Certificate subject
     * @param issuer        Certificate issuer
     * @param validityStart Validity start date of certificate in msecs
     * @param validityEnd   Validity end date of certificate in msecs
     * @param publicKey     Public part of key pair
     * @param privateKey    Private part of key pair
     * @param signatureType Signature Type
     * @param serialNumber  Serial number
     * @param extensions    Extensions, ignored by version 1 generators
     * @return The generated certificate
     * @throws CryptoException If there was a problem generating the certificate
     */
    public X509Certificate generate(X500Name subject, X500Name issuer, Date validityStart, Date validityEnd,
                                    PublicKey publicKey, PrivateKey privateKey, SignatureType signatureType,
                                    BigInteger serialNumber, X509Extension extensions, Provider provider)
            throws CryptoException {
        if (version == X509CertificateVersion.VERSION1) {
            // TODO
            return generateVersion1(subject, issuer, validityStart, validityEnd, publicKey, privateKey, signatureType,
                                    serialNumber);
        } else {
            try {
                return generateVersion3(subject, issuer, validityStart, validityEnd, publicKey, privateKey,
                                        signatureType, serialNumber, extensions, provider);
            } catch (CertIOException e) {
                throw new CryptoException(e);
            }
        }
    }

    /**
     * Generate a self-signed certificate.
     *
     * @param name          Certificate subject and issuer
     * @param validity      Validity period of certificate in msecs
     * @param publicKey     Public part of key pair
     * @param privateKey    Private part of key pair
     * @param signatureType Signature Type
     * @param serialNumber  Serial number
     * @return The generated certificate
     * @throws CryptoException If there was a problem generating the certificate
     */
    public X509Certificate generateSelfSigned(X500Name name, long validity, PublicKey publicKey, PrivateKey privateKey,
                                              SignatureType signatureType, BigInteger serialNumber)
            throws CryptoException {
        Date validityStart = new Date();
        Date validityEnd = new Date(validityStart.getTime() + validity);
        return generateSelfSigned(name, validityStart, validityEnd, publicKey, privateKey, signatureType, serialNumber);
    }

    /**
     * Generate a self-signed certificate.
     *
     * @param name          Certificate subject and issuer
     * @param validityStart Validity start date of certificate in msecs
     * @param validityEnd   Validity end date of certificate in msecs
     * @param publicKey     Public part of key pair
     * @param privateKey    Private part of key pair
     * @param signatureType Signature Type
     * @param serialNumber  Serial number
     * @return The generated certificate
     * @throws CryptoException If there was a problem generating the certificate
     */
    public X509Certificate generateSelfSigned(X500Name name, Date validityStart, Date validityEnd, PublicKey publicKey,
                                              PrivateKey privateKey, SignatureType signatureType,
                                              BigInteger serialNumber) throws CryptoException {
        return generateSelfSigned(name, validityStart, validityEnd, publicKey, privateKey, signatureType, serialNumber,
                                  null, KSE.BC);
    }

    /**
     * Generate a self-signed certificate.
     *
     * @param name          Certificate subject and issuer
     * @param validityStart Validity start date of certificate in msecs
     * @param validityEnd   Validity end date of certificate in msecs
     * @param publicKey     Public part of key pair
     * @param privateKey    Private part of key pair
     * @param signatureType Signature Type
     * @param serialNumber  Serial number
     * @param extensions    Extensions, ignored by version 1 generators
     * @return The generated certificate
     * @throws CryptoException If there was a problem generating the certificate
     */
    public X509Certificate generateSelfSigned(X500Name name, Date validityStart, Date validityEnd, PublicKey publicKey,
                                              PrivateKey privateKey, SignatureType signatureType,
                                              BigInteger serialNumber, X509Extension extensions, Provider provider)
            throws CryptoException {
        return generate(name, name, validityStart, validityEnd, publicKey, privateKey, signatureType, serialNumber,
                        extensions, provider);
    }

    private X509Certificate generateVersion1(X500Name subject, X500Name issuer, Date validityStart, Date validityEnd,
                                             PublicKey publicKey, PrivateKey privateKey, SignatureType signatureType,
                                             BigInteger serialNumber) throws CryptoException {
        Date notBefore = validityStart == null ? new Date() : validityStart;
        Date notAfter = validityEnd == null ? new Date(notBefore.getTime() + TimeUnit.DAYS.toMillis(365)) : validityEnd;

        JcaX509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder(issuer, serialNumber, notBefore,
                                                                                  notAfter, subject, publicKey);

        try {
            ContentSigner certSigner = new JcaContentSignerBuilder(signatureType.jce()).setProvider(KSE.BC)
                                                                                       .build(privateKey);
            return new JcaX509CertificateConverter().setProvider(KSE.BC).getCertificate(certBuilder.build(certSigner));
        } catch (CertificateException | IllegalStateException | OperatorCreationException ex) {
            throw new CryptoException(res.getString("CertificateGenFailed.exception.message"), ex);
        }
    }

    private X509Certificate generateVersion3(X500Name subject, X500Name issuer, Date validityStart, Date validityEnd,
                                             PublicKey publicKey, PrivateKey privateKey, SignatureType signatureType,
                                             BigInteger serialNumber, X509Extension extensions, Provider provider)
            throws CryptoException, CertIOException {
        Date notBefore = validityStart == null ? new Date() : validityStart;
        Date notAfter = validityEnd == null ? new Date(notBefore.getTime() + TimeUnit.DAYS.toMillis(365)) : validityEnd;

        JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(issuer, serialNumber, notBefore,
                                                                                  notAfter, subject, publicKey);

        if (extensions != null) {
            for (String oid : extensions.getCriticalExtensionOIDs()) {
                certBuilder.addExtension(new ASN1ObjectIdentifier(oid), true, getExtensionValue(extensions, oid));
            }

            for (String oid : extensions.getNonCriticalExtensionOIDs()) {
                certBuilder.addExtension(new ASN1ObjectIdentifier(oid), false, getExtensionValue(extensions, oid));
            }
        }

        try {
            ContentSigner certSigner = null;

            if (provider == null) {
                certSigner = new JcaContentSignerBuilder(signatureType.jce()).setProvider(KSE.BC).build(privateKey);
            } else {
                certSigner = new JcaContentSignerBuilder(signatureType.jce()).setProvider(provider).build(privateKey);
            }

            return new JcaX509CertificateConverter().setProvider(KSE.BC).getCertificate(certBuilder.build(certSigner));
        } catch (CertificateException | IllegalStateException | OperatorCreationException ex) {
            throw new CryptoException(res.getString("CertificateGenFailed.exception.message"), ex);
        }
    }

    private ASN1Encodable getExtensionValue(X509Extension extensions, String oid) throws CryptoException {
        byte[] octets = ASN1OctetString.getInstance(extensions.getExtensionValue(oid)).getOctets();
        try (ASN1InputStream ais = new ASN1InputStream(octets)) {
            return ais.readObject();
        } catch (IOException ex) {
            throw new CryptoException(res.getString("CertificateGenFailed.exception.message"), ex);
        }
    }
}
