/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.sshd.openpgp;

import java.io.IOException;
import java.io.InputStream;
import java.io.StreamCorruptedException;
import java.net.ProtocolException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.loader.AbstractKeyPairResourceParser;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.bouncycastle.openpgp.PGPException;
import org.c02e.jpgpj.Key;
import org.c02e.jpgpj.Subkey;

/**
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public class PGPKeyPairResourceParser
        extends AbstractKeyPairResourceParser
        implements PGPKeyLoader,
        PGPPublicKeyExtractor,
        PGPPrivateKeyExtractor {
    public static final String BEGIN_MARKER = "BEGIN PGP PRIVATE KEY BLOCK";
    public static final List<String> BEGINNERS = Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER));

    public static final String END_MARKER = "END PGP PRIVATE KEY BLOCK";
    public static final List<String> ENDERS = Collections.unmodifiableList(Collections.singletonList(END_MARKER));

    public static final PGPKeyPairResourceParser INSTANCE = new PGPKeyPairResourceParser();

    public PGPKeyPairResourceParser() {
        super(BEGINNERS, ENDERS);
    }

    @Override
    public Collection<KeyPair> extractKeyPairs(
            SessionContext session, NamedResource resourceKey,
            String beginMarker, String endMarker,
            FilePasswordProvider passwordProvider,
            List<String> lines, Map<String, String> headers)
            throws IOException, GeneralSecurityException {
        // We need to re-construct the original data - including start/end markers
        String eol = System.lineSeparator();
        int numLines = GenericUtils.size(lines);
        StringBuilder sb = new StringBuilder(
                beginMarker.length() + endMarker.length() + 4 + numLines * 80)
                .append(beginMarker);
        if (numLines > 0) {
            for (String l : lines) {
                sb.append(eol).append(l);
            }
        }
        sb.append(eol).append(endMarker).append(eol);

        String keyData = sb.toString();
        byte[] dataBytes = keyData.getBytes(StandardCharsets.US_ASCII);
        try {
            return extractKeyPairs(session, resourceKey, beginMarker, endMarker, passwordProvider, dataBytes, headers);
        } finally {
            Arrays.fill(dataBytes, (byte) 0); // clean up sensitive data a.s.a.p.
        }
    }

    @Override
    public Collection<KeyPair> extractKeyPairs(
            SessionContext session, NamedResource resourceKey,
            String beginMarker, String endMarker,
            FilePasswordProvider passwordProvider,
            InputStream stream, Map<String, String> headers)
            throws IOException, GeneralSecurityException {
        for (int retryCount = 0;; retryCount++) {
            String password = (passwordProvider == null)
                    ? null
                    : passwordProvider.getPassword(session, resourceKey, retryCount);
            Collection<KeyPair> keys;
            try {
                if (retryCount > 0) {
                    stream.reset();
                }

                Key key = PGPKeyLoader.loadPGPKey(stream, password);
                keys = extractKeyPairs(resourceKey, key.getSubkeys());
            } catch (IOException | GeneralSecurityException | PGPException | RuntimeException e) {
                ResourceDecodeResult result = (passwordProvider != null)
                        ? passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryCount, password, e)
                        : ResourceDecodeResult.TERMINATE;
                if (result == null) {
                    result = ResourceDecodeResult.TERMINATE;
                }

                switch (result) {
                    case TERMINATE:
                        if (e instanceof PGPException) {
                            throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
                                                               + " to decode " + resourceKey + ": " + e.getMessage());
                        } else if (e instanceof IOException) {
                            throw (IOException) e;
                        } else if (e instanceof GeneralSecurityException) {
                            throw (GeneralSecurityException) e;
                        } else {
                            throw (RuntimeException) e;
                        }
                    case RETRY:
                        continue;
                    case IGNORE:
                        return Collections.emptyList();
                    default:
                        throw new ProtocolException("Unsupported decode attempt result (" + result + ") for " + resourceKey);
                }
            }

            if (passwordProvider != null) {
                passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryCount, password, null);
            }

            return keys;
        }
    }

    public List<KeyPair> extractKeyPairs(NamedResource resourceKey, Collection<? extends Subkey> subKeys)
            throws IOException, GeneralSecurityException {
        if (GenericUtils.isEmpty(subKeys)) {
            return Collections.emptyList();
        }

        List<KeyPair> kpList = new ArrayList<>(subKeys.size());
        boolean debugEnabled = log.isDebugEnabled();
        for (Subkey sk : subKeys) {
            PublicKey pubKey;
            try {
                pubKey = extractPublicKey(resourceKey, sk);
                if (pubKey == null) {
                    if (debugEnabled) {
                        log.debug("extractKeyPairs({}) no public key extracted from {}", resourceKey, sk);
                    }
                    continue;
                }
            } catch (IOException | GeneralSecurityException | RuntimeException | Error e) {
                log.error("extractKeyPairs({}) failed ({}) to extract public key of {}: {}",
                        resourceKey, e.getClass().getSimpleName(), sk, e.getMessage());
                throw e;
            }

            PrivateKey prvKey;
            try {
                prvKey = extractPrivateKey(resourceKey, sk, pubKey);
                if (prvKey == null) {
                    if (debugEnabled) {
                        log.debug("extractKeyPairs({}) no private key extracted from {}", resourceKey, sk);
                    }
                    continue;
                }
            } catch (IOException | GeneralSecurityException | RuntimeException | Error e) {
                log.error("extractKeyPairs({}) failed ({}) to extract private key of {}: {}",
                        resourceKey, e.getClass().getSimpleName(), sk, e.getMessage());
                throw e;
            } catch (PGPException e) {
                log.error("extractKeyPairs({}) failed ({}) to parse private key of {}: {}",
                        resourceKey, e.getClass().getSimpleName(), sk, e.getMessage());
                throw new StreamCorruptedException("Failed to parse " + resourceKey + " sub-key=" + sk + ": " + e.getMessage());
            }

            KeyPair kp = new KeyPair(pubKey, prvKey);
            KeyPair prev = kpList.isEmpty()
                    ? null
                    : kpList.stream()
                            .filter(e -> KeyUtils.compareKeyPairs(e, kp))
                            .findFirst()
                            .orElse(null);
            if (prev != null) {
                if (debugEnabled) {
                    log.debug("extractKeyPairs({}) skip duplicate sub-key={}", resourceKey, sk);
                }
                continue;
            }

            kpList.add(kp);
        }

        return kpList;
    }

    @Override
    public <K extends PublicKey> K generatePublicKey(String algorithm, Class<K> keyType, KeySpec keySpec)
            throws GeneralSecurityException {
        KeyFactory factory = getKeyFactory(algorithm);
        PublicKey pubKey = factory.generatePublic(keySpec);
        return keyType.cast(pubKey);
    }

    @Override
    public <K extends PrivateKey> K generatePrivateKey(String algorithm, Class<K> keyType, KeySpec keySpec)
            throws GeneralSecurityException {
        KeyFactory factory = getKeyFactory(algorithm);
        PrivateKey prvKey = factory.generatePrivate(keySpec);
        return keyType.cast(prvKey);
    }

    protected KeyFactory getKeyFactory(String algorithm) throws GeneralSecurityException {
        return SecurityUtils.getKeyFactory(algorithm);
    }
}
