/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.core.util.validation;

import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidatorException;
import java.security.cert.Certificate;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.PKIXReason;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.Objects;
import java.util.Set;
import javax.security.auth.x500.X500Principal;

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

/**
 * A {@link PKIXCertPathChecker} that does the same basic checks as
 * sun.security.provider.certpath.BasicChecker but allows for the validation
 * check to be suppressed.
 *
 * Not currently used because the BasicChecker isn't replaceable. Needs to be
 * used in conjuction with {@link OpcUaCertPathValidator} if/when it's
 * implemented.
 */
public class OpcUaCertificateValidityChecker extends PKIXCertPathChecker {

    private static final Logger LOGGER =
        LoggerFactory.getLogger(OpcUaCertificateValidityChecker.class);

    private X500Principal previousSubject;
    private PublicKey previousPublicKey;

    private final CertPath certPath;
    private final TrustAnchor trustAnchor;
    private final Set<ValidationCheck> validationChecks;

    public OpcUaCertificateValidityChecker(
        CertPath certPath,
        TrustAnchor trustAnchor,
        Set<ValidationCheck> validationChecks
    ) {

        this.certPath = certPath;
        this.trustAnchor = trustAnchor;
        this.validationChecks = validationChecks;
    }

    @Override
    public void init(boolean forward) throws CertPathValidatorException {
        if (!forward) {
            if (trustAnchor.getTrustedCert() != null) {
                previousPublicKey = trustAnchor.getTrustedCert().getPublicKey();
                previousSubject = trustAnchor.getTrustedCert().getSubjectX500Principal();
            } else {
                previousPublicKey = trustAnchor.getCAPublicKey();
                previousSubject = trustAnchor.getCA();
            }
        } else {
            throw new
                CertPathValidatorException("forward checking not supported");
        }
    }

    @Override
    public boolean isForwardCheckingSupported() {
        return false;
    }

    @Override
    public Set<String> getSupportedExtensions() {
        return null;
    }

    @Override
    public void check(Certificate cert, Collection<String> unresolvedCritExts) throws CertPathValidatorException {
        X509Certificate certificate = (X509Certificate) cert;

        try {
            verifyValidity(certificate);
            verifyNameChaining(certificate);
            verifySignature(certificate);
        } catch (CertPathValidatorException e) {
            if (validationChecks.contains(ValidationCheck.VALIDITY)) {
                throw e;
            } else {
                LOGGER.warn(
                    "check suppressed: certificate failed validity check: {}",
                    certificate.getSubjectX500Principal().getName()
                );
            }
        }

        updateInternalState(certificate);
    }

    private void updateInternalState(X509Certificate certificate) {
        previousPublicKey = certificate.getPublicKey();
        previousSubject = certificate.getSubjectX500Principal();
    }

    private void verifyValidity(X509Certificate certificate) throws CertPathValidatorException {
        try {
            certificate.checkValidity(new Date());
        } catch (CertificateExpiredException e) {
            throw new CertPathValidatorException(
                "validity check failed (expired)",
                e,
                null,
                certPath.getCertificates().indexOf(certificate),
                CertPathValidatorException.BasicReason.EXPIRED
            );
        } catch (CertificateNotYetValidException e) {
            throw new CertPathValidatorException(
                "validity check failed (not yet valid)",
                e,
                null,
                certPath.getCertificates().indexOf(certificate),
                CertPathValidatorException.BasicReason.NOT_YET_VALID
            );
        }
    }

    private void verifyNameChaining(X509Certificate certificate) throws CertPathValidatorException {
        if (previousSubject != null) {
            X500Principal issuer = certificate.getIssuerX500Principal();

            if (issuer.getName() == null || issuer.getName().isEmpty()) {
                throw new CertPathValidatorException(
                    "subject/issuer name chaining check failed: null/empty issuer DN",
                    null,
                    null,
                    certPath.getCertificates().indexOf(certificate),
                    PKIXReason.NAME_CHAINING
                );
            }

            if (!Objects.equals(issuer, previousSubject)) {
                throw new CertPathValidatorException(
                    "subject/issuer name chaining check failed",
                    null,
                    null,
                    certPath.getCertificates().indexOf(certificate),
                    PKIXReason.NAME_CHAINING
                );
            }
        }
    }

    private void verifySignature(X509Certificate certificate) throws CertPathValidatorException {
        try {
            certificate.verify(previousPublicKey);
        } catch (SignatureException e) {
            throw new CertPathValidatorException(
                "signature check failed",
                e,
                null,
                certPath.getCertificates().indexOf(certificate),
                CertPathValidatorException.BasicReason.INVALID_SIGNATURE
            );
        } catch (GeneralSecurityException e) {
            throw new CertPathValidatorException("signature check failed", e);
        }
    }

}
