package org.apereo.cas.adaptors.x509.authentication.revocation.checker;

import org.apereo.cas.adaptors.x509.authentication.revocation.RevokedCertificateException;
import org.apereo.cas.adaptors.x509.authentication.revocation.policy.DenyRevocationPolicy;
import org.apereo.cas.adaptors.x509.authentication.revocation.policy.RevocationPolicy;
import org.apereo.cas.adaptors.x509.authentication.revocation.policy.ThresholdExpiredCRLRevocationPolicy;
import org.apereo.cas.adaptors.x509.util.CertUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.GeneralSecurityException;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Base class for all CRL-based revocation checkers.
 *
 * @author Marvin S. Addison
 * @since 3.4.6
 */
public abstract class AbstractCRLRevocationChecker implements RevocationChecker {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCRLRevocationChecker.class);

    /**
     * Flag to indicate whether all
     * crls should be checked for the cert resource.
     * Defaults to {@code false}.
     **/
    protected final boolean checkAll;

    /**
     * Policy to apply when CRL data is unavailable.
     */
    private final RevocationPolicy<Void> unavailableCRLPolicy;

    /**
     * Policy to apply when CRL data has expired.
     */
    private final RevocationPolicy<X509CRL> expiredCRLPolicy;

    /**
     * Instantiates a new Abstract crl revocation checker.
     *
     * @param checkAll             Indicates whether all resources should be checked,
     *                             or revocation should stop at the first resource
     *                             that produces the cert.
     * @param unavailableCRLPolicy the unavailable crl policy
     * @param expiredCRLPolicy     the expired crl policy
     */
    public AbstractCRLRevocationChecker(final boolean checkAll,
                                        final RevocationPolicy<Void> unavailableCRLPolicy,
                                        final RevocationPolicy<X509CRL> expiredCRLPolicy) {
        this.checkAll = checkAll;
        this.unavailableCRLPolicy = unavailableCRLPolicy == null
                ? new DenyRevocationPolicy() : unavailableCRLPolicy;
        this.expiredCRLPolicy = expiredCRLPolicy == null
                ? new ThresholdExpiredCRLRevocationPolicy(0) : expiredCRLPolicy;
    }

    @Override
    public void check(final X509Certificate cert) throws GeneralSecurityException {
        if (cert == null) {
            throw new IllegalArgumentException("Certificate cannot be null.");
        }
        LOGGER.debug("Evaluating certificate revocation status for [{}]", CertUtils.toString(cert));
        final Collection<X509CRL> crls = getCRLs(cert);

        if (crls == null || crls.isEmpty()) {
            LOGGER.warn("CRL data is not available for [{}]", CertUtils.toString(cert));
            this.unavailableCRLPolicy.apply(null);
            return;
        }

        final List<X509CRL> expiredCrls = new ArrayList<>();
        final List<X509CRLEntry> revokedCrls;

        crls.stream().filter(CertUtils::isExpired).forEach(crl -> {
            LOGGER.warn("CRL data expired on [{}]", crl.getNextUpdate());
            expiredCrls.add(crl);
        });

        if (crls.size() == expiredCrls.size()) {
            LOGGER.warn("All CRLs retrieved have expired. Applying CRL expiration policy...");
            for (final X509CRL crl : expiredCrls) {
                this.expiredCRLPolicy.apply(crl);
            }
        } else {
            crls.removeAll(expiredCrls);
            LOGGER.debug("Valid CRLs [{}] found that are not expired yet", crls);

            revokedCrls = crls.stream().map(crl -> crl.getRevokedCertificate(cert)).filter(Objects::nonNull).collect(Collectors.toList());

            if (revokedCrls.size() == crls.size()) {
                final X509CRLEntry entry = revokedCrls.get(0);
                LOGGER.warn("All CRL entries have been revoked. Rejecting the first entry [{}]", entry);
                throw new RevokedCertificateException(entry);
            }
        }
    }

    public RevocationPolicy<Void> getUnavailableCRLPolicy() {
        return this.unavailableCRLPolicy;
    }

    public RevocationPolicy<X509CRL> getExpiredCRLPolicy() {
        return this.expiredCRLPolicy;
    }

    /**
     * Records the addition of a new CRL entry.
     *
     * @param id  the id of the entry to keep track of
     * @param crl new CRL entry
     * @return true if the entry was added successfully.
     * @since 4.1
     */
    protected abstract boolean addCRL(Object id, X509CRL crl);

    /**
     * Gets the collection of CRLs for the given certificate.
     *
     * @param cert Certificate for which the CRL of the issuing CA should be retrieved.
     * @return CRLs for given cert.
     */
    protected abstract Collection<X509CRL> getCRLs(X509Certificate cert);
}
