// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0

package org.pgpainless.decryption_verification;

import static org.pgpainless.signature.consumer.SignatureValidator.signatureWasCreatedInBounds;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.pgpainless.PGPainless;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.policy.Policy;
import org.pgpainless.signature.consumer.CertificateValidator;
import org.pgpainless.signature.consumer.DetachedSignatureCheck;
import org.pgpainless.signature.consumer.OnePassSignatureCheck;
import org.pgpainless.signature.SignatureUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class SignatureInputStream extends FilterInputStream {

    protected SignatureInputStream(InputStream inputStream) {
        super(inputStream);
    }

    public static class VerifySignatures extends SignatureInputStream {

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

        private final PGPObjectFactory objectFactory;
        private final List<OnePassSignatureCheck> opSignatures;
        private final Map<Long, OnePassSignatureCheck> opSignaturesWithMissingCert;
        private final List<DetachedSignatureCheck> detachedSignatures;
        private final ConsumerOptions options;
        private final OpenPgpMetadata.Builder resultBuilder;

        public VerifySignatures(
                InputStream literalDataStream,
                @Nullable PGPObjectFactory objectFactory,
                List<OnePassSignatureCheck> opSignatures,
                Map<Long, OnePassSignatureCheck> onePassSignaturesWithMissingCert,
                List<DetachedSignatureCheck> detachedSignatures,
                ConsumerOptions options,
                OpenPgpMetadata.Builder resultBuilder) {
            super(literalDataStream);
            this.objectFactory = objectFactory;
            this.opSignatures = opSignatures;
            this.opSignaturesWithMissingCert = onePassSignaturesWithMissingCert;
            this.detachedSignatures = detachedSignatures;
            this.options = options;
            this.resultBuilder = resultBuilder;
        }

        @Override
        public int read() throws IOException {
            final int data = super.read();
            final boolean endOfStream = data == -1;
            if (endOfStream) {
                finalizeSignatures();
            } else {
                byte b = (byte) data;
                updateOnePassSignatures(b);
                updateDetachedSignatures(b);
            }
            return data;
        }

        @Override
        public int read(@Nonnull byte[] b, int off, int len) throws IOException {
            int read = super.read(b, off, len);

            final boolean endOfStream = read == -1;
            if (endOfStream) {
                finalizeSignatures();
            } else {
                updateOnePassSignatures(b, off, read);
                updateDetachedSignatures(b, off, read);
            }
            return read;
        }

        private void finalizeSignatures() {
            parseAndCombineSignatures();
            verifyOnePassSignatures();
            verifyDetachedSignatures();
        }

        public void parseAndCombineSignatures() {
            if (objectFactory == null) {
                return;
            }
            // Parse signatures from message
            PGPSignatureList signatures;
            try {
                signatures = parseSignatures(objectFactory);
            } catch (IOException e) {
                return;
            }
            List<PGPSignature> signatureList = SignatureUtils.toList(signatures);
            // Set signatures as comparison sigs in OPS checks
            for (int i = 0; i < opSignatures.size(); i++) {
                int reversedIndex = opSignatures.size() - i - 1;
                opSignatures.get(i).setSignature(signatureList.get(reversedIndex));
            }

            for (PGPSignature signature : signatureList) {
                if (opSignaturesWithMissingCert.containsKey(signature.getKeyID())) {
                    OnePassSignatureCheck check = opSignaturesWithMissingCert.remove(signature.getKeyID());
                    check.setSignature(signature);

                    resultBuilder.addInvalidInbandSignature(new SignatureVerification(signature, null),
                            new SignatureValidationException(
                                    "Missing verification certificate " + Long.toHexString(signature.getKeyID())));
                }
            }
        }

        private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException {
            PGPSignatureList signatureList = null;
            Object pgpObject = objectFactory.nextObject();
            while (pgpObject !=  null && signatureList == null) {
                if (pgpObject instanceof PGPSignatureList) {
                    signatureList = (PGPSignatureList) pgpObject;
                } else {
                    pgpObject = objectFactory.nextObject();
                }
            }

            if (signatureList == null || signatureList.isEmpty()) {
                throw new IOException("Verification failed - No Signatures found");
            }

            return signatureList;
        }


        private synchronized void verifyOnePassSignatures() {
            Policy policy = PGPainless.getPolicy();
            for (OnePassSignatureCheck opSignature : opSignatures) {
                if (opSignature.getSignature() == null) {
                    LOGGER.warn("Found OnePassSignature without respective signature packet -> skip");
                    continue;
                }

                try {
                    signatureWasCreatedInBounds(options.getVerifyNotBefore(),
                            options.getVerifyNotAfter()).verify(opSignature.getSignature());
                    CertificateValidator.validateCertificateAndVerifyOnePassSignature(opSignature, policy);
                    resultBuilder.addVerifiedInbandSignature(
                            new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()));
                } catch (SignatureValidationException e) {
                    LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}",
                            opSignature.getSigningKey(), e.getMessage(), e);
                    resultBuilder.addInvalidInbandSignature(
                            new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()), e);
                }
            }
        }

        private void verifyDetachedSignatures() {
            Policy policy = PGPainless.getPolicy();
            for (DetachedSignatureCheck s : detachedSignatures) {
                try {
                    signatureWasCreatedInBounds(options.getVerifyNotBefore(),
                            options.getVerifyNotAfter()).verify(s.getSignature());
                    CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(),
                            (PGPPublicKeyRing) s.getSigningKeyRing(), policy);
                    resultBuilder.addVerifiedDetachedSignature(new SignatureVerification(s.getSignature(),
                            s.getSigningKeyIdentifier()));
                } catch (SignatureValidationException e) {
                    LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}",
                            s.getSigningKeyIdentifier(), e.getMessage(), e);
                    resultBuilder.addInvalidDetachedSignature(new SignatureVerification(s.getSignature(),
                            s.getSigningKeyIdentifier()), e);
                }
            }
        }

        private void updateOnePassSignatures(byte data) {
            for (OnePassSignatureCheck opSignature : opSignatures) {
                opSignature.getOnePassSignature().update(data);
            }
        }

        private void updateOnePassSignatures(byte[] bytes, int offset, int length) {
            for (OnePassSignatureCheck opSignature : opSignatures) {
                opSignature.getOnePassSignature().update(bytes, offset, length);
            }
        }

        private void updateDetachedSignatures(byte b) {
            for (DetachedSignatureCheck detachedSignature : detachedSignatures) {
                detachedSignature.getSignature().update(b);
            }
        }

        private void updateDetachedSignatures(byte[] b, int off, int read) {
            for (DetachedSignatureCheck detachedSignature : detachedSignatures) {
                detachedSignature.getSignature().update(b, off, read);
            }
        }

    }
}
