/*
 * 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.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
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.TreeMap;

import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.keyprovider.MappedKeyPairProvider;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
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");
    }

    private static final class LazyDefaultUserHomeFolderHolder {
        private static final Path PATH =
                new File(ValidateUtils.checkNotNullAndNotEmpty(System.getProperty("user.home"), "No user home"))
                        .toPath()
                        .toAbsolutePath()
                        .normalize();
    }

    /**
     * @return The {@link Path} to the currently running user home
     */
    @SuppressWarnings("synthetic-access")
    public static Path getUserHomeFolder() {
        return LazyDefaultUserHomeFolderHolder.PATH;
    }

    /**
     * @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 (GenericUtils.isEmpty(ids)) {
            return null;
        }

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

            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 (GenericUtils.isEmpty(pairsMap)) {
            return null;
        } else {
            return new MappedKeyPairProvider(pairsMap);
        }
    }

    /**
     * @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 {@link FilePasswordProvider#getPassword(String)} 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 Map} 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
     * @see SecurityUtils#loadKeyPairIdentity(String, InputStream, FilePasswordProvider)
     */
    public static Map<String, KeyPair> loadIdentities(Map<String, ? extends Path> paths, FilePasswordProvider provider, OpenOption... options)
            throws IOException, GeneralSecurityException {
        if (GenericUtils.isEmpty(paths)) {
            return Collections.emptyMap();
        }

        Map<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();
            try (InputStream inputStream = Files.newInputStream(path, options)) {
                KeyPair kp = SecurityUtils.loadKeyPairIdentity(path.toString(), inputStream, provider);
                KeyPair prev = ids.put(type, kp);
                ValidateUtils.checkTrue(prev == null, "Multiple keys for type=%s", type);
            }
        }

        return ids;
    }
}
