/*
 * 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.common.config.keys;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Collections;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;

import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.keyprovider.MappedKeyPairProvider;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.resource.PathResource;
import org.apache.sshd.common.util.security.SecurityUtils;

/**
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public final class IdentityUtils {
    private IdentityUtils() {
        throw new UnsupportedOperationException("No instance");
    }

    /**
     * @param  prefix The file name prefix - ignored if {@code null}/empty
     * @param  type   The identity type - ignored if {@code null}/empty
     * @param  suffix The file name suffix - ignored if {@code null}/empty
     * @return        The identity file name or {@code null} if no name
     */
    public static String getIdentityFileName(String prefix, String type, String suffix) {
        if (GenericUtils.isEmpty(type)) {
            return null;
        } else {
            return GenericUtils.trimToEmpty(prefix)
                   + type.toLowerCase()
                   + GenericUtils.trimToEmpty(suffix);
        }
    }

    /**
     * @param  ids           A {@link Map} of the loaded identities where key=the identity type, value=the matching
     *                       {@link KeyPair} - ignored if {@code null}/empty
     * @param  supportedOnly If {@code true} then ignore identities that are not supported internally
     * @return               A {@link KeyPair} for the identities - {@code null} if no identities available (e.g., after
     *                       filtering unsupported ones)
     * @see                  BuiltinIdentities
     */
    public static KeyPairProvider createKeyPairProvider(Map<String, KeyPair> ids, boolean supportedOnly) {
        if (MapEntryUtils.isEmpty(ids)) {
            return null;
        }

        Map<String, KeyPair> pairsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        ids.forEach((type, kp) -> {
            BuiltinIdentities id = BuiltinIdentities.fromName(type);

            if (supportedOnly && ((id == null) || (!id.isSupported()))) {
                return;
            }

            String keyType = KeyUtils.getKeyType(kp);
            if (GenericUtils.isEmpty(keyType)) {
                return;
            }

            KeyPair prev = pairsMap.put(keyType, kp);
            if (prev != null) {
                return; // less of an offense if 2 pairs mapped to same key type
            }
        });

        if (MapEntryUtils.isEmpty(pairsMap)) {
            return null;
        } else {
            return new MappedKeyPairProvider(pairsMap);
        }
    }

    /**
     * @param  session                  The {@link SessionContext} for invoking this load command - may be {@code null}
     *                                  if not invoked within a session context (e.g., offline tool or session unknown).
     * @param  paths                    A {@link Map} of the identities where key=identity type (case
     *                                  <U>insensitive</U>), value=the {@link Path} of file with the identity key
     * @param  provider                 A {@link FilePasswordProvider} - may be {@code null} if the loaded keys are
     *                                  <U>guaranteed</U> not to be encrypted. The argument to
     *                                  {@code FilePasswordProvider#getPassword} is the path of the file whose key is to
     *                                  be loaded
     * @param  options                  The {@link OpenOption}s to use when reading the key data
     * @return                          A {@link NavigableMap} of the identities where key=identity type (case
     *                                  <U>insensitive</U>), value=the {@link KeyPair} of the identity
     * @throws IOException              If failed to access the file system
     * @throws GeneralSecurityException If failed to load the keys
     */
    public static NavigableMap<String, KeyPair> loadIdentities(
            SessionContext session, Map<String, ? extends Path> paths, FilePasswordProvider provider, OpenOption... options)
            throws IOException, GeneralSecurityException {
        if (MapEntryUtils.isEmpty(paths)) {
            return Collections.emptyNavigableMap();
        }

        NavigableMap<String, KeyPair> ids = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        // Cannot use forEach because the potential for IOExceptions being thrown
        for (Map.Entry<String, ? extends Path> pe : paths.entrySet()) {
            String type = pe.getKey();
            Path path = pe.getValue();
            PathResource location = new PathResource(path, options);
            Iterable<KeyPair> pairs;
            try (InputStream inputStream = location.openInputStream()) {
                pairs = SecurityUtils.loadKeyPairIdentities(session, location, inputStream, provider);
            }

            if (pairs == null) {
                continue;
            }

            for (KeyPair kp : pairs) {
                KeyPair prev = ids.put(type, kp);
                ValidateUtils.checkTrue(prev == null, "Multiple keys for type=%s in %s", type, path);
            }
        }

        return ids;
    }
}
