/*
 * 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.karaf.shell.ssh;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Base64;

public class KnownHostsManager {
    Logger LOG = LoggerFactory.getLogger(KnownHostsManager.class);

    private final File knownHosts;

    public KnownHostsManager(File knownHosts) {
        this.knownHosts = knownHosts;
        this.knownHosts.getParentFile().mkdirs();
        if (!this.knownHosts.exists()) {
            try {
                knownHosts.createNewFile();
            } catch (IOException e) {
                throw new RuntimeException("Error creating file for known hosts at: " + knownHosts);
            }
        }
    }

    public PublicKey getKnownKey(SocketAddress remoteAddress, String checkAlgorithm)
        throws InvalidKeySpecException {
        FileReader fr = null;
        BufferedReader reader = null;
        try {
            fr = new FileReader(knownHosts);
            reader = new BufferedReader(fr);
            return getKnownKeyInternal(remoteAddress, checkAlgorithm, reader);
        } catch (IOException e) {
            throw new RuntimeException("Error reading known_hosts " + knownHosts, e);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } finally {
            close(reader);
            close(fr);
        }
    }

    private PublicKey getKnownKeyInternal(SocketAddress remoteAddress, String checkAlgorithm,
                                          BufferedReader reader)
        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        String checkServerAddress = getAddressString(remoteAddress);

        String line = reader.readLine();
        while (line != null) {
            String[] lineParts = line.split(" ");
            String serverAddress = lineParts[0];
            String algorithm = lineParts[1];
            if (checkServerAddress.equals(serverAddress) && checkAlgorithm.equals(algorithm)) {
                byte[] key = Base64.getDecoder().decode(lineParts[2].getBytes());
                KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
                X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key);
                return keyFactory.generatePublic(keySpec);
            }
            line = reader.readLine();
        }
        return null;
    }

    public void storeKeyForHost(SocketAddress remoteAddress, PublicKey serverKey) {
        FileWriter ps = null;
        BufferedWriter bw = null;
        try {
            ps = new FileWriter(knownHosts, true);
            bw = new BufferedWriter(ps);
            writeKey(bw, remoteAddress, serverKey);
        } catch (Exception e) {
            throw new RuntimeException("Error storing key for host" + remoteAddress, e);
        } finally {
            close(bw);
            close(ps);
        }
    }

    private void writeKey(BufferedWriter bw, SocketAddress remoteAddress, PublicKey serverKey)
        throws IOException {
        bw.append(getAddressString(remoteAddress));
        bw.append(" ");
        bw.append(serverKey.getAlgorithm());
        bw.append(" ");
        serverKey.getEncoded();
        bw.append(new String(Base64.getEncoder().encode(serverKey.getEncoded()), StandardCharsets.UTF_8));
        bw.append("\n");
    }

    String getAddressString(SocketAddress address) {
        if (address instanceof InetSocketAddress) {
            InetSocketAddress inetAddress = (InetSocketAddress)address;
            return String.format("%s,%s:%s", inetAddress.getHostName(),
                                 inetAddress.getAddress().getHostAddress(), inetAddress.getPort());
        }
        return "";
    }

    private void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                LOG.warn("Error closing: " + e.getMessage(), e);
            }
        }
    }

}
