package org.cagrid.gaards.dorian.ca;

import gov.nih.nci.cagrid.common.FaultHelper;

import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;

import org.bouncycastle.asn1.x509.X509Name;
import org.cagrid.gaards.dorian.common.Lifetime;
import org.cagrid.gaards.dorian.common.LoggingObject;
import org.cagrid.gaards.dorian.service.util.Utils;
import org.cagrid.gaards.pki.CRLEntry;
import org.cagrid.gaards.pki.CertUtil;
import org.cagrid.gaards.pki.KeyUtil;


/**
 * @author <A href="mailto:langella@bmi.osu.edu">Stephen Langella </A>
 * @author <A href="mailto:oster@bmi.osu.edu">Scott Oster </A>
 * @author <A href="mailto:hastings@bmi.osu.edu">Shannon Hastings </A>
 * @version $Id: ArgumentManagerTable.java,v 1.2 2004/10/15 16:35:16 langella
 *          Exp $
 */
public abstract class CertificateAuthority extends LoggingObject {

    private boolean initialized = false;

    private CertificateAuthorityProperties properties;


    public CertificateAuthority(CertificateAuthorityProperties properties) {
        this.properties = properties;
    }


    public abstract String getUserCredentialsProvider();


    public abstract String getCACredentialsProvider();


    public abstract String getSignatureAlgorithm();


    public abstract boolean hasCACredentials() throws CertificateAuthorityFault;


    public abstract void setCACredentials(X509Certificate cert, PrivateKey key, String password)
        throws CertificateAuthorityFault;


    public abstract void deleteCACredentials() throws CertificateAuthorityFault;


    public abstract PrivateKey getPrivateKey(String password) throws CertificateAuthorityFault, NoCACredentialsFault;


    protected abstract java.security.cert.X509Certificate getCertificate() throws CertificateAuthorityFault;


    public void clearCertificateAuthority() throws CertificateAuthorityFault {
        deleteCACredentials();
        this.initialized = false;
    }


    private synchronized void init() throws CertificateAuthorityFault {
        try {
            if (!initialized) {
                if (!hasCACredentials()) {
                    if (properties.isAutoCreateCAEnabled()) {
                        Lifetime lifetime = properties.getCreationPolicy().getLifetime();
                        this.createCertifcateAuthorityCredentials(properties.getCreationPolicy().getSubject(), Utils
                            .getExpiredDate(lifetime), properties.getCreationPolicy().getKeySize());
                    }
                }
                initialized = true;
            }
        } catch (Exception e) {
            logError(e.getMessage(), e);
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Unexpected Error, could not initialize the Dorian Certificate Authority.");
            FaultHelper helper = new FaultHelper(fault);
            helper.addFaultCause(e);
            fault = (CertificateAuthorityFault) helper.getFault();
            throw fault;
        }
    }


    private void createCertifcateAuthorityCredentials(String dn, Date expirationDate, int keySize)
        throws CertificateAuthorityFault, NoCACredentialsFault {
        try {
            KeyPair pair = KeyUtil.generateRSAKeyPair(getCACredentialsProvider(), keySize);
            X509Certificate cacert = CertUtil.generateCACertificate(getCACredentialsProvider(), new X509Name(dn),
                new Date(), expirationDate, pair, getSignatureAlgorithm());
            deleteCACredentials();
            this.setCACredentials(cacert, pair.getPrivate(), properties.getCertificateAuthorityPassword());
        } catch (Exception e) {
            logError(e.getMessage(), e);
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Unexpected Error, could not create the CA credentials.");
            FaultHelper helper = new FaultHelper(fault);
            helper.addFaultCause(e);
            fault = (CertificateAuthorityFault) helper.getFault();
            throw fault;
        }
    }


    public java.security.cert.X509Certificate getCACertificate() throws CertificateAuthorityFault, NoCACredentialsFault {
        return getCACertificate(true);
    }


    private java.security.cert.X509Certificate getCACertificate(boolean errorOnExpiredCredentials)
        throws CertificateAuthorityFault, NoCACredentialsFault {
        X509Certificate cert = null;
        init();
        try {
            if (!hasCACredentials()) {
                NoCACredentialsFault fault = new NoCACredentialsFault();
                fault.setFaultString("No certificate exists for the CA.");
                throw fault;
            } else {
                cert = getCertificate();
            }
        } catch (NoCACredentialsFault f) {
            throw f;
        } catch (Exception e) {
            logError(e.getMessage(), e);
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Unexpected Error, Error obtaining the CA private key.");
            FaultHelper helper = new FaultHelper(fault);
            helper.addFaultCause(e);
            fault = (CertificateAuthorityFault) helper.getFault();
            throw fault;
        }

        if (errorOnExpiredCredentials) {
            Date now = new Date();
            if (now.before(cert.getNotBefore()) || (now.after(cert.getNotAfter()))) {
                if (properties.isAutoRenewCAEnabled()) {
                    Lifetime lifetime = properties.getRenewalLifetime();
                    return renewCertifcateAuthorityCredentials(Utils.getExpiredDate(lifetime));

                } else {
                    NoCACredentialsFault fault = new NoCACredentialsFault();
                    fault.setFaultString("The CA certificate had expired or is not valid at this time.");
                    throw fault;
                }
            }
        }
        return cert;
    }


    public synchronized X509Certificate signCertificate(String subject, PublicKey publicKey, Date start, Date expiration)
        throws CertificateAuthorityFault, NoCACredentialsFault {
        init();
        X509Certificate cacert = getCACertificate();

        Date caDate = cacert.getNotAfter();
        if (start.after(caDate)) {
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Certificate start date is after the CA certificates expiration date.");
            throw fault;
        }
        if (expiration.after(caDate)) {
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Certificate expiration date is after the CA certificates expiration date.");
            throw fault;
        }

        try {
            // VALIDATE DN
            String caSubject = cacert.getSubjectDN().getName();
            int caindex = caSubject.lastIndexOf(",");
            String caPreSub = caSubject.substring(0, caindex);

            if (!subject.startsWith(caPreSub)) {
                CertificateAuthorityFault fault = new CertificateAuthorityFault();
                fault.setFaultString("Invalid certificate subject, the subject must start with, " + caPreSub);
                throw fault;
            }
            X509Certificate cert = CertUtil.generateCertificate(getCACredentialsProvider(), new X509Name(subject),
                start, expiration, publicKey, cacert, getPrivateKey(), getSignatureAlgorithm(), properties
                    .getPolicyOID());
            return cert;
        } catch (CertificateAuthorityFault e) {
            throw e;
        } catch (Exception e) {
            logError(e.getMessage(), e);
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Unexpected Error, could not sign certificate.");
            FaultHelper helper = new FaultHelper(fault);
            helper.addFaultCause(e);
            fault = (CertificateAuthorityFault) helper.getFault();
            throw fault;
        }
    }


    public synchronized X509Certificate signHostCertificate(String host, PublicKey publicKey, Date start,
        Date expiration) throws CertificateAuthorityFault, NoCACredentialsFault {
        init();
        X509Certificate cacert = getCACertificate();
        try {
            String subject = Utils.getHostCertificateSubject(cacert, host);
            return signCertificate(subject, publicKey, start, expiration);
        } catch (Exception e) {
            logError(e.getMessage(), e);
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Unexpected Error, could not sign host certificate.");
            FaultHelper helper = new FaultHelper(fault);
            helper.addFaultCause(e);
            fault = (CertificateAuthorityFault) helper.getFault();
            throw fault;
        }
    }


    public synchronized X509Certificate renewCertifcateAuthorityCredentials(Date expirationDate)
        throws CertificateAuthorityFault, NoCACredentialsFault {
        init();
        try {
            X509Certificate oldcert = getCACertificate(false);
            int size = ((RSAPublicKey) oldcert.getPublicKey()).getModulus().bitLength();
            KeyPair pair = KeyUtil.generateRSAKeyPair(getCACredentialsProvider(), size);
            X509Certificate cacert = CertUtil.generateCACertificate(getCACredentialsProvider(), new X509Name(oldcert
                .getSubjectDN().getName()), new Date(), expirationDate, pair, getSignatureAlgorithm());
            deleteCACredentials();
            this.setCACredentials(cacert, pair.getPrivate(), properties.getCertificateAuthorityPassword());
            return cacert;
        } catch (Exception e) {
            logError(e.getMessage(), e);
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Unexpected Error, could renew the CA credentials.");
            FaultHelper helper = new FaultHelper(fault);
            helper.addFaultCause(e);
            fault = (CertificateAuthorityFault) helper.getFault();
            throw fault;
        }
    }


    public X509CRL getCRL(CRLEntry[] entries) throws CertificateAuthorityFault, NoCACredentialsFault {
        try {
            init();
            return CertUtil.createCRL(getCACredentialsProvider(), getCACertificate(), getPrivateKey(), entries,
                getCACertificate().getNotAfter(), getSignatureAlgorithm());
        } catch (Exception e) {
            logError(e.getMessage(), e);
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Unexpected Error, could not create the CRL.");
            FaultHelper helper = new FaultHelper(fault);
            helper.addFaultCause(e);
            fault = (CertificateAuthorityFault) helper.getFault();
            throw fault;
        }
    }


    public PrivateKey getPrivateKey() throws CertificateAuthorityFault, NoCACredentialsFault {
        init();
        try {
            if (!hasCACredentials()) {
                NoCACredentialsFault fault = new NoCACredentialsFault();
                fault.setFaultString("No Private Key exists for the CA.");
                throw fault;
            } else {
                return getPrivateKey(properties.getCertificateAuthorityPassword());
            }
        } catch (NoCACredentialsFault f) {
            throw f;
        } catch (Exception e) {
            logError(e.getMessage(), e);
            CertificateAuthorityFault fault = new CertificateAuthorityFault();
            fault.setFaultString("Unexpected Error, Error obtaining the CA private key.");
            FaultHelper helper = new FaultHelper(fault);
            helper.addFaultCause(e);
            fault = (CertificateAuthorityFault) helper.getFault();
            throw fault;
        }
    }


    public CertificateAuthorityProperties getProperties() {
        return properties;
    }
}
