package org.xbib.net.security.ssl.keymanager;

import java.net.Socket;
import java.net.URI;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.X509ExtendedKeyManager;
import org.xbib.net.security.ssl.util.KeyManagerUtils;

/**
 * Represents an ordered list of {@link X509ExtendedKeyManager} with most-preferred managers first.
 * This is necessary because of the fine-print on {@link javax.net.ssl.SSLContext#init}:
 * Only the first instance of a particular key and/or key manager implementation type in the
 * array is used. (For example, only the first javax.net.ssl.X509KeyManager in the array will be used.)
 * The KeyManager can be build from one or more of any combination provided within the
 * {@link KeyManagerUtils.KeyManagerBuilder KeyManagerUtils.KeyManagerBuilder}.
 * This includes:
 * <pre>
 *     - Any amount of custom KeyManagers
 *     - Any amount of custom Identities
 * </pre>
 *
 * <strong>NOTE:</strong>
 * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time.
 * Instead use the {@link KeyManagerUtils KeyManagerUtils} which provides the same functionality
 * while it has a stable API because it is part of the public API.
 *
 * @see <a href="http://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm">
 *     http://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm
 *     </a>
 * @see <a href="http://codyaray.com/2013/04/java-ssl-with-multiple-keystores">
 *     http://codyaray.com/2013/04/java-ssl-with-multiple-keystores
 *     </a>
 */
public final class CompositeX509ExtendedKeyManager extends X509ExtendedKeyManager implements CombinableX509ExtendedKeyManager, RoutableX509ExtendedKeyManager {

    private final List<X509ExtendedKeyManager> keyManagers;

    private final Map<String, List<URI>> preferredAliasToHost;

    /**
     * Creates a new {@link CompositeX509ExtendedKeyManager}.
     *
     * @param keyManagers the {@link X509ExtendedKeyManager}, ordered with the most-preferred managers first.
     */
    public CompositeX509ExtendedKeyManager(List<? extends X509ExtendedKeyManager> keyManagers) {
        this(keyManagers, Collections.emptyMap());
    }

    /**
     * Creates a new {@link CompositeX509ExtendedKeyManager}.
     *
     * @param keyManagers          the {@link X509ExtendedKeyManager}, ordered with the most-preferred managers first.
     * @param preferredAliasToHost the preferred client alias to be used for the given host
     */
    public CompositeX509ExtendedKeyManager(List<? extends X509ExtendedKeyManager> keyManagers,
                                           Map<String, List<URI>> preferredAliasToHost) {
        this.keyManagers = Collections.unmodifiableList(keyManagers);
        this.preferredAliasToHost = new HashMap<>(preferredAliasToHost);
    }

    /**
     * Chooses the first non-null client alias returned from the delegate
     * {@link X509ExtendedKeyManager}, or {@code null} if there are no matches.
     */
    @Override
    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
        return chooseClientAlias(
                socket,
                this::containsInetSocketAddress,
                this::extractHostAndPort,
                keyManager -> keyManager.chooseClientAlias(keyType, issuers, socket)
        );
    }

    /**
     * Chooses the first non-null client alias returned from the delegate
     * {@link X509ExtendedKeyManager}, or {@code null} if there are no matches.
     */
    @Override
    public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine sslEngine) {
        return chooseClientAlias(
                sslEngine,
                Objects::nonNull,
                this::extractHostAndPort,
                keyManager -> keyManager.chooseEngineClientAlias(keyTypes, issuers, sslEngine)
        );
    }

    /**
     * Chooses the first non-null server alias returned from the delegate
     * {@link X509ExtendedKeyManager}, or {@code null} if there are no matches.
     */
    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
        return chooseServerAlias(
                socket,
                SSLSocket.class::isInstance,
                aSocket -> ((SSLSocket) aSocket).getHandshakeSession(),
                keyManager -> keyManager.chooseServerAlias(keyType, issuers, socket)
        );
    }

    /**
     * Chooses the first non-null server alias returned from the delegate
     * {@link X509ExtendedKeyManager}, or {@code null} if there are no matches.
     */
    @Override
    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine sslEngine) {
        return chooseServerAlias(
                sslEngine,
                Objects::nonNull,
                SSLEngine::getHandshakeSession,
                keyManager -> keyManager.chooseEngineServerAlias(keyType, issuers, sslEngine)
        );
    }

    /**
     * Returns the first non-null private key associated with the
     * given alias, or {@code null} if the alias can't be found.
     */
    @Override
    public PrivateKey getPrivateKey(String alias) {
        return extractInnerField(
                keyManager -> keyManager.getPrivateKey(alias),
                Objects::nonNull
        );
    }

    /**
     * Returns the first non-null certificate chain associated with the
     * given alias, or {@code null} if the alias can't be found.
     */
    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        return extractInnerField(
                keyManager -> keyManager.getCertificateChain(alias),
                chain -> chain != null && chain.length > 0
        );
    }

    /**
     * Get all matching aliases for authenticating the client side of a
     * secure socket, or {@code null} if there are no matches.
     */
    @Override
    public String[] getClientAliases(String keyType, Principal[] issuers) {
        return getAliases(keyManager -> keyManager.getClientAliases(keyType, issuers));
    }

    /**
     * Get all matching aliases for authenticating the server side of a
     * secure socket, or {@code null} if there are no matches.
     */
    @Override
    public String[] getServerAliases(String keyType, Principal[] issuers) {
        return getAliases(keyManager -> keyManager.getServerAliases(keyType, issuers));
    }

    @Override
    public List<X509ExtendedKeyManager> getKeyManagers() {
        return keyManagers;
    }

    @Override
    public Map<String, List<URI>> getIdentityRoute() {
        return preferredAliasToHost;
    }

}
