/**
 * 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.storm.security.auth;

import com.google.common.annotations.VisibleForTesting;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import javax.security.auth.Subject;
import javax.xml.bind.DatatypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This plugin is intended to be used for user topologies to send SSL keystore/truststore files to the remote workers. On the client side,
 * this takes the files specified in ssl.credential.files, reads the file contents, base64's it, converts it to a String, and adds it to the
 * credentials map. The key in the credentials map is the name of the file. On the worker side it uses the filenames from the
 * ssl.credential.files config to lookup the keys in the credentials map and decodes it and writes it back out as a file.
 *
 * User is responsible for referencing them from the topology code as ./<filename>.
 */
public class AutoSSL implements IAutoCredentials {
    public static final String SSL_FILES_CONF = "ssl.credential.files";
    private static final Logger LOG = LoggerFactory.getLogger(AutoSSL.class);
    private Map<String, Object> conf;
    private String writeDir = "./";

    // Adds the serialized and base64 file to the credentials map as a string with the filename as
    // the key.
    public static void serializeSSLFile(String readFile, Map<String, String> credentials) {
        try (FileInputStream in = new FileInputStream(readFile)) {
            LOG.debug("serializing ssl file: {}", readFile);
            ByteArrayOutputStream result = new ByteArrayOutputStream();
            byte[] buffer = new byte[4096];
            int length;
            while ((length = in.read(buffer)) != -1) {
                result.write(buffer, 0, length);
            }
            String resultStr = DatatypeConverter.printBase64Binary(result.toByteArray());

            File f = new File(readFile);
            LOG.debug("ssl read files is name: {}", f.getName());
            credentials.put(f.getName(), resultStr);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void deserializeSSLFile(String credsKey, String directory,
                                          Map<String, String> credentials) {
        try {
            LOG.debug("deserializing ssl file with key: {}", credsKey);
            String resultStr = null;

            if (credentials != null &&
                credentials.containsKey(credsKey) &&
                credentials.get(credsKey) != null) {
                resultStr = credentials.get(credsKey);
            }
            if (resultStr != null) {
                byte[] decodedData = DatatypeConverter.parseBase64Binary(resultStr);
                File f = new File(directory, credsKey);
                try (FileOutputStream fout = new FileOutputStream(f)) {
                    fout.write(decodedData);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void prepare(Map<String, Object> conf) {
        this.conf = conf;
        writeDir = getSSLWriteDirFromConf(this.conf);
    }

    @VisibleForTesting
    protected String getSSLWriteDirFromConf(Map<String, Object> conf) {
        return "./";
    }

    @VisibleForTesting
    Collection<String> getSSLFilesFromConf(Map<String, Object> conf) {
        Object sslConf = conf.get(SSL_FILES_CONF);
        if (sslConf == null) {
            LOG.info("No ssl files requested, if you want to use SSL please set {} to the " +
                     "list of files", SSL_FILES_CONF);
            return null;
        }
        Collection<String> sslFiles = null;
        if (sslConf instanceof Collection) {
            sslFiles = (Collection<String>) sslConf;
        } else if (sslConf instanceof String) {
            sslFiles = Arrays.asList(((String) sslConf).split(","));
        } else {
            throw new RuntimeException(
                SSL_FILES_CONF + " is not set to something that I know how to use " + sslConf);
        }
        return sslFiles;
    }

    @Override
    public void populateCredentials(Map<String, String> credentials) {
        try {
            Collection<String> sslFiles = getSSLFilesFromConf(conf);
            if (sslFiles == null) {
                return;
            }
            LOG.info("AutoSSL files: {}", sslFiles);
            for (String inputFile : sslFiles) {
                serializeSSLFile(inputFile, credentials);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void updateSubject(Subject subject, Map<String, String> credentials) {
        populateSubject(subject, credentials);
    }

    @Override
    public void populateSubject(Subject subject, Map<String, String> credentials) {
        LOG.info("AutoSSL populating credentials");
        Collection<String> sslFiles = getSSLFilesFromConf(conf);
        if (sslFiles == null) {
            LOG.debug("ssl files is null");
            return;
        }
        for (String outputFile : sslFiles) {
            deserializeSSLFile(new File(outputFile).getName(), writeDir, credentials);
        }
    }
}
