/*
 * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.launcher;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.io.PrintStream;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import jdk.internal.access.SharedSecrets;

/**
 * A utility class for security libs functionality
 * in the -XshowSettings:security output
 */
public final class SecuritySettings {

    private static final String INDENT = "    ";
    private static final String TWOINDENT = INDENT + INDENT;
    private static final String THREEINDENT = TWOINDENT + INDENT;
    private static final String PROV_INFO_STRING = "Provider information: ";
    private static PrintStream ostream = null;

    static void printSecuritySettings(LauncherHelper.Option o, PrintStream stream, boolean verbose) {
        ostream = stream;
        if (!verbose) {
            printSecuritySummarySettings();
            return;
        }
        switch (o) {
            case SECURITY_PROPERTIES -> printSecurityProperties();
            case SECURITY_PROVIDERS -> printSecurityProviderConfig(true);
            case SECURITY_TLS -> printSecurityTLSConfig(true);
            case SECURITY, SECURITY_ALL -> printAllSecurityConfig();
        }
    }

    // A non-verbose description of some core security configuration settings
    static void printSecuritySummarySettings() {
        ostream.println("Security settings summary:");
        ostream.println(INDENT + "Use \"-XshowSettings:security\" " +
                "option for verbose security settings options");
        printSecurityProviderConfig(false);
        printSecurityTLSConfig(false);
    }

    static void printAllSecurityConfig() {
        ostream.println("Security settings:");
        printSecurityProperties();
        printSecurityProviderConfig(true);
        printSecurityTLSConfig(true);
    }

    private static void printSecurityProperties() {
        ostream.println(INDENT + "Security properties:");
        Properties p = SharedSecrets.getJavaSecurityPropertiesAccess().getInitialProperties();
        for (String key : p.stringPropertyNames().stream().sorted().toList()) {
            String val = p.getProperty(key);
            if (val.length() > 60) {
                splitLongPropertyLines(key, val);
            } else {
                ostream.println(TWOINDENT + key + "=" + val);
            }
        }
        ostream.println();
    }

    private static void splitLongPropertyLines(String key, String val) {
        // split long property values which use well known separator
        if (val.contains(",") || val.contains(";")) {
            String separator = (val.contains(",")) ? "," : ";";
            ostream.println(TWOINDENT + key + "=");
            String[] values = val.split(separator);
            String lastValue = values[values.length -1].trim();
            List.of(values).forEach(
                    s -> ostream.println(THREEINDENT + s.trim() +
                            (s.trim().equals(lastValue) ? "" : separator)));
        } else {
            ostream.println(TWOINDENT + key + "=" + val);
        }
    }

    private static void printSecurityTLSConfig(boolean verbose) {
        SSLSocket ssls;
        SSLContext sslContext;
        try {
            sslContext = SSLContext.getDefault();
            ssls = (SSLSocket)sslContext.getSocketFactory().createSocket();
        } catch (IOException | NoSuchAlgorithmException e) {
            ostream.println(INDENT + "Failed to create SSL socket");
            ostream.println(INDENT + e + "\n");
            return;
        }

        ostream.println(INDENT + "Security TLS configuration (" +
                sslContext.getProvider().getName() + " provider):");
        ostream.println(TWOINDENT + "Enabled Protocols:");
        for (String s : ssls.getEnabledProtocols()) {
            ostream.println(THREEINDENT + s);
        }

        if (verbose) {
            ostream.println("\n" + TWOINDENT + "Enabled Cipher Suites:");
            for (String s : ssls.getEnabledCipherSuites()) {
                ostream.println(THREEINDENT + s);
            }
        }
        ostream.println();
    }

    private static void printSecurityProviderConfig(boolean verbose) {
        ostream.println(INDENT + "Security provider static configuration: (in order of preference)");
        for (Provider p : Security.getProviders()) {
            if (verbose) {
                // separate the views out
                ostream.println(TWOINDENT + "-".repeat(40));
            }
            ostream.println(TWOINDENT + "Provider name: " + p.getName());
            if (verbose) {
                ostream.println(wrappedString(PROV_INFO_STRING + p.getInfo(), 80,
                        TWOINDENT, THREEINDENT));
                ostream.println(TWOINDENT + "Provider services: (type : algorithm)");
                Set<Provider.Service> services = p.getServices();
                Set<String> keys = Collections.list(p.keys())
                        .stream()
                        .map(String.class::cast)
                        .filter(s -> s.startsWith("Alg.Alias."))
                        .collect(Collectors.toSet());
                if (!services.isEmpty()) {
                    services.stream()
                            .sorted(Comparator.comparing(Provider.Service::getType)
                                    .thenComparing(Provider.Service::getAlgorithm))
                            .forEach(ps -> {
                                ostream.println(THREEINDENT +
                                        ps.getType() + "." + ps.getAlgorithm());
                                List<String> aliases = keys
                                        .stream()
                                        .filter(s -> s.startsWith("Alg.Alias." + ps.getType()))
                                        .filter(s -> p.getProperty(s).equals(ps.getAlgorithm()))
                                        .map(s -> s.substring(("Alg.Alias." + ps.getType() + ".").length()))
                                        .toList();

                                if (!aliases.isEmpty()) {
                                    ostream.println(wrappedString(
                                            aliases.stream()
                                                    .collect(Collectors.joining(", ", INDENT + " aliases: [", "]")),
                                            80, " " + TWOINDENT, INDENT + THREEINDENT));
                                }
                            });
                } else {
                    ostream.println(THREEINDENT + "<none>");
                }
            }
        }
        if (verbose) {
            ostream.println();
        }
    }

    // return a string split across multiple lines which aims to limit max length
    private static String wrappedString(String orig, int limit,
                                        String initIndent, String successiveIndent) {
        if (orig == null || orig.isEmpty() || limit <= 0) {
            // bad input
            return orig;
        }
        StringBuilder sb = new StringBuilder();
        int widthCount = 0;
        for (String s : orig.split(" ")) {
            if (widthCount == 0) {
                // first iteration only
                sb.append(initIndent + s);
                widthCount = s.length() + initIndent.length();
            } else {
                if (widthCount + s.length() > limit) {
                    sb.append("\n" + successiveIndent + s);
                    widthCount = s.length() + successiveIndent.length();
                } else {
                    sb.append(" " + s);
                    widthCount += s.length() + 1;
                }
            }
        }
        return sb.toString();
    }
}
