package be.iminds.ilabt.jfed.ui.commandline.probe;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.ApiInfo;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.api.*;
import be.iminds.ilabt.jfed.lowlevel.api_scripts.GetUsersInProject;
import be.iminds.ilabt.jfed.lowlevel.connection.*;
import be.iminds.ilabt.jfed.preferences.JFedCorePreferences;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.ui.commandline.BaseCli;
import be.iminds.ilabt.jfed.util.*;
import com.google.inject.Injector;
import org.apache.commons.cli.*;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.Objects;

/**
 * CommandLineClient
 */
public class CommandLineClient {
    private final static org.slf4j.Logger LOG = LoggerFactory.getLogger(CommandLineClient.class);

    public static void help(PrintStream out, Options options) {
        PrintWriter pw = new PrintWriter(out);
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp(pw, 200, "jfed-probe-cli [options ... ] <command> [command arguments ...]", "Possible options:", options,
                formatter.defaultLeftPad, formatter.defaultDescPad,
                "Required options:\n" +
                        "         --api\n" +
                        "         either --context-file or both -k and -u    (-u can be replaced by -a)\n");
        pw.close();
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws IOException, JFedException {
        int retval = main_helper(args, System.out, System.err, System.in, new BaseCli(System.err), true);
        System.exit(retval);
    }

    public static int main_helper(String[] args,
                                  final PrintStream out, final PrintStream err, final InputStream in,
                                  BaseCli baseCli,
                                  boolean temporarilyRedirectSystemStreams) throws IOException, JFedException {
        final PrintStream orig_out = System.out;
        final PrintStream orig_err = System.err;
        final InputStream orig_in = System.in;
        if (temporarilyRedirectSystemStreams) {
            System.setOut(out);
            System.setErr(err);
            System.setIn(in);
        }

        int retval = -1;
        try {
            retval = main_internal(args, out, err, in, baseCli);
        } finally {
            if (temporarilyRedirectSystemStreams) {
                System.setOut(orig_out);
                System.setErr(orig_err);
                System.setIn(orig_in);
            }
        }
        return retval;
    }

    public static int main_internal(String[] args, final PrintStream out, final PrintStream err, final InputStream in,
                                    BaseCli baseCli) throws IOException, JFedException {
        CommandLineParser parser = new BasicParser();

        // create the Options
        Options options = new Options();
        //////////// TARGET config ////////////
        options.addOption(Option.builder().longOpt("api")
                .desc("The API to call (MANDATORY)")
                .hasArg()
                .argName("API")
                .required()
                .build());
        options.addOption("u", "server-url", true, "URL of server");
        options.addOption("a", "authority", true, "fill in server url based on hrn. Only works for known authorities");

        //////////// USER config ////////////
        BaseCli.addUserConfig(options, "replaces all previous parameters by those in the provided context properties file (same format as used by automated tester)", true);
//        options.addOption("k", "key-file", true, "file containing user certificate and key in PEM format");
//        options.addOption(OptionBuilder.longOpt("context-file")
//                .desc("replaces all previous parameters by those in the provided context properties file (same format as used by automated tester)")
//                .hasArg()
//                .argName("CONTEXT_FILE")
//                .create());

        //////////// AUTHORITIES config ////////////
        BaseCli.addAuthoritiesConfig(options);
//        options.addOption(OptionBuilder.longOpt("authorities-file")
//                .desc("The xml file containing the list of known authorities. Default: ~/.jFed/authorities.xml")
//                .hasArg()
//                .argName("AUTHORITIES XML FILE")
//                .create());

        //////////// CLI Specific config ////////////

        options.addOption(Option.builder().longOpt("output-file")
                .desc("file to write output to")
                .hasArg()
                .argName("OUTPUT_FILE")
                .build());
        options.addOption(Option.builder().longOpt("use-tested-SA")
                .desc("do not use the SA of the user, use the SA of the tested authority")
                .build());
        options.addOption(Option.builder().longOpt("accept-self-signed")
                .desc("accept unknown self signed certificates (DANGER: this is a big security risk)")
                .build());
        options.addOption("q", "quiet", false, "less output");
        options.addOption("d", "debug", false, "extra debugging output");
        options.addOption("i", "interactive", false, "interactively request some missing options and command arguments. The --api option and the <command> are still required");

        CommandLine line = null;
        try {
            // parse the command line arguments
            line = parser.parse(options, args);
        } catch (ParseException exp) {
            err.println("Command line argument Syntax error: " + exp.getMessage());
            help(err, options);
            return 1;
        }

        boolean useTestedSA = line.hasOption("use-tested-SA");
        boolean debug = line.hasOption("debug");
        String outputfile = line.getOptionValue("output-file");
        boolean silent = line.hasOption("quiet");
        boolean interactive = line.hasOption("interactive");
        boolean acceptSelfSigned = line.hasOption("accept-self-signed");
        String serverUrlStr = line.getOptionValue("server-url");
        String auth = line.getOptionValue("authority");

        List<String> arguments = line.getArgList();
        if (arguments.size() < 1) {
            err.println("Error: No command specified after options.");
            help(err, options);
            return 1;
        }


        baseCli.initAuthoritiesModule(line);
        try {
            baseCli.initUserModule(line, out, err, in);
        } catch (BaseCli.CliArgumentException e) {
            LOG.debug("Got CliArgumentException", e);
            //User has been informed already
            return 1;
        }

        //first check if mandatory options are given (we don;t do this using the library, because it's either context-file or it is a number of other options  )
        boolean contextExists = line.hasOption("context-file");

        if (line.hasOption("u") && line.hasOption("a")) {
            err.println("You can only use either -u or -a, not both");
            help(err, options);
            return 1;
        }
        boolean connectionDetailsExist = (line.hasOption("k") || line.hasOption("p")) && (line.hasOption("u") || line.hasOption("a"));
        if (contextExists && connectionDetailsExist) {
            err.println("--context-file cannot be mixed with -u, -k- and -a");
            help(err, options);
            return 1;
        }
        if (!interactive && !contextExists && !connectionDetailsExist) {
            err.println("illegal combination of --context-file and -u, -k-, -a");
            help(err, options);
            return 1;
        }


//        SfaAuthority.debug = false; //ugly hack to suppress output


//        String wall3Am2ServerUrl = "https://www.wall3.test.ibbt.be/protogeni/xmlrpc/am/2.0";
//        String wall3SaServerUrl = "https://www.wall3.test.ibbt.be/protogeni/xmlrpc/sa";
//        String chServerUrl = "https://www.emulab.net/protogeni/xmlrpc/ch";


        String command = arguments.remove(0);
        if (command.equalsIgnoreCase("GetUsersInProject")) {
            if (arguments.size() != 2) {
                System.err.println("GetUsersInProject requires 2 arguments (PROJECT_NAME and USERS_DIR). Not " + arguments.size() + ": " + arguments);
                return 1;
            }

            Injector injector = baseCli.getInjector(err);
            GeniUser user = injector.getInstance(GeniUser.class);

            if (!BaseCli.checkUser(user, line, out, err, in))
                return 1;

            GetUsersInProject getUsersInProject = injector.getInstance(GetUsersInProject.class);

            String projectName = arguments.remove(0);
            String usersDirString = arguments.remove(0);
            File usersDir = new File(usersDirString);
            getUsersInProject.save(projectName, usersDir);

            return 0;
        }

        //no need to inject logger. Not doing hat makes things easier
        Logger logger = new Logger(); //injector.getInstance(Logger.class);

        JFedPreferences jFedPreferences = new JFedCorePreferences();

        CommandLineCore core = null;
        ApiInfo.Api api = null;
        String apiArg = line.getOptionValue("api");
        if (apiArg.equalsIgnoreCase("AM2") || apiArg.equalsIgnoreCase("AMv2")) {
            api = new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 2);
            core = new CommandLineCore<>(AggregateManager2.class, new AggregateManager2(logger, jFedPreferences));
        }
        if (apiArg.equalsIgnoreCase("MA2")) {
            api = new ApiInfo.Api(ApiInfo.ApiName.GENI_CH_MA, 2);
            core =
                    new CommandLineCore<>(FederationMemberAuthorityApi2.class, new FederationMemberAuthorityApi2(logger, jFedPreferences));
        }
        if (apiArg.equalsIgnoreCase("SA2")) {
            api = new ApiInfo.Api(ApiInfo.ApiName.GENI_CH_SA, 2);
            core =
                    new CommandLineCore<>(FederationSliceAuthorityApi2.class, new FederationSliceAuthorityApi2(logger, jFedPreferences));
        }
        if (apiArg.equalsIgnoreCase("AM3") || apiArg.equalsIgnoreCase("AMv3")) {
            api = new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 3);
            core = new CommandLineCore<>(AggregateManager3.class, new AggregateManager3(logger, jFedPreferences));
        }
        if (apiArg.equalsIgnoreCase("PROTOGENI_SA")) {
            api = new ApiInfo.Api(ApiInfo.ApiName.PROTOGENI_SA, 1);
            core =
                    new CommandLineCore<>(ProtogeniSliceAuthority.class, new ProtogeniSliceAuthority(logger, jFedPreferences));
        }
        if (apiArg.equalsIgnoreCase("PROTOGENI_CH")) {
            api = new ApiInfo.Api(ApiInfo.ApiName.PROTOGENI_CH, 1);
            core =
                    new CommandLineCore<>(ProtoGeniClearingHouse1.class, new ProtoGeniClearingHouse1(logger, jFedPreferences));
        }
        if (apiArg.equalsIgnoreCase("PROTOGENI_AM_EXT")) {
            api = new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 2);
            core =
                    new CommandLineCore<>(ProtoGeniAMExtensions.class, new ProtoGeniAMExtensions(logger, jFedPreferences));
        }
        if (core == null || api == null) {
            err.println("Error: unknown API: \"" + apiArg + "\"  (registered here: PROTOGENI_SA, PROTOGENI_CH, AM2, AM3, PROTOGENI_EXT_AM)");
            help(err, options);
            return 1;
        }

        if (core != null && !core.getAvailableMethodNamesLowerCase().contains(command.toLowerCase())) {
            err.println("Error: Unknown command: '" + command + "'");
            help(err, options);
            return 1;
        }


        JFedConnection con;
        if (!line.hasOption("context-file")) {
            Service targetService = null;
            Server targetServer = null;

            Injector injector = baseCli.getInjector(err);
            TestbedInfoSource testbedInfoSource = injector.getInstance(TestbedInfoSource.class);
            GeniUser user = injector.getInstance(GeniUser.class);
            assert user != null;
            assert user.getUserAuthorityServer() != null;

            boolean asked = false;

            if (serverUrlStr == null) {
                if (auth != null) {
                    if (auth.startsWith("urn:publicid")) {
                        targetServer = testbedInfoSource.getFromAnyUrnExact(auth); //CLIS is exact: user has to specify exact urn.
                        // In case a difference between rspec and contacted server is needed, the server is specified here, and the rspec is specified as arg by user anyway, instead of generated.
                        targetService = ApiInfo.findService(targetServer, api);
                        serverUrlStr = targetService == null ? null : targetService.getUrl();
                    } else {
                        for (Server knownAuth : testbedInfoSource.getServers()) {
                            if (Objects.equals(knownAuth.getName(), auth)) {
                                targetService = ApiInfo.findService(knownAuth, api);
                                serverUrlStr = targetService == null ? null : targetService.getUrl();
                                targetServer = knownAuth;
                            }
                        }
                    }
                    if (targetServer == null) {
                        err.println("ERROR: Authority '" + auth + "' is unknown.");
                        return -1;
                    }
                    if (targetService == null ||  serverUrlStr == null) {
                        err.println("ERROR: Server URL for authority '" + auth + "' and server type " + apiArg + " is unknown.");
                        return -1;
                    }
                }
                if (serverUrlStr == null) {
                    assert interactive;
                    serverUrlStr = IOUtils.askCommandLineInput("Server URL", in);
                    asked = true;
                }
            }
            if (targetServer == null) {
                //find authority for server
                URL server = new URL(serverUrlStr);
                String serverHostname = server.getHost();
                for (Server knownAuth : testbedInfoSource.getServers()) {
                    for (Service service : knownAuth.getServices()) {
                        URL url = new URL(service.getUrl());
                        if (Objects.equals(url.getHost(), serverHostname)) {
                            targetServer = knownAuth;
                            targetService = service;
                        }
                    }
                }
            }
            if (targetServer == null) {
                out.println("WARNING: No known authority matches server URL. Will check if certificate is trusted.");

                SSLCertificateDownloader.SSLCertificateJFedInfo info =
                        SSLCertificateDownloader.getCertificateInfo(new URL(serverUrlStr));

                out.println("Note: Unknown authority, so created temporary fake authority info, in order to connect to server.");

                String urnString = "urn:publicid:IDN+fake+authority+fake";

                TestbedBuilder testbedBuilder = new TestbedBuilder();
                ServerBuilder serverBuilder = new ServerBuilder();
                testbedBuilder.addServerBuilder(serverBuilder);
                testbedBuilder.setLongName("Temporarily created fake authority for URL " + serverUrlStr);
                serverBuilder.setName("Temporarily created fake authority for URL " + serverUrlStr);
                serverBuilder.setServerType("temp-fake-unknown");
                testbedBuilder.setDefaultComponentManagerUrn(urnString);
                serverBuilder.setDefaultComponentManagerUrn(urnString);

                ServiceBuilder serviceBuilder = new ServiceBuilder();
                serviceBuilder.setApi(api.getId());
                serviceBuilder.setApiVersion(api.getVersion());
                serviceBuilder.setUrl(serverUrlStr);
                serviceBuilder.setUrn(urnString);
                serverBuilder.addServiceBuilder(serviceBuilder);

                if (info.isSelfSigned()) {
                    //TODO currently this assumes the certificate is not trusted, but we don't know that for sure. We should check...

                    boolean trust = acceptSelfSigned;
                    if (!trust) {
                        if (!interactive) {
                            err.println("FATAL: The server's self-signed certificate is not trusted! Either add the authority to the list, or add a command line option to trust any self signed certificate (dangerous).");
                            return 1;
                        }
                        out.println("Note: The server's self-signed certificate is not trusted! Either add the authority to the list, or trust it temporarily. Certificate:");
                        out.println(KeyUtil.x509certificateToPem(info.getCert()));
                        assert interactive;
                        String trustSelfSignedCert =
                                IOUtils.askCommandLineInput("Trust self-signed certificate (Unsafe!)? (y/N)", in);
                        trust = trustSelfSignedCert.equalsIgnoreCase("y");
                    }

                    if (trust) {
                        if (info != null && !info.isTrusted()) {
                            if (info.getSubjectMatchesHostname() == Boolean.FALSE) {
                                serverBuilder.setAllowedCertificateAlias(info.getSubject());
                            }
                            serverBuilder.setCertificateChain(KeyUtil.x509certificateToPem(info.getCert()));
                        }
                    } else {
                        err.println("FATAL: the server's certificate is not trusted. Connecting would fail.");
                        return 1;
                    }
                }
                Testbed fakeTestbed = testbedBuilder.create();
                Server fakeServer = fakeTestbed.getServers().get(0);
                targetServer = fakeServer;
                targetService = fakeServer.getServices().get(0);
            }
            assert targetServer != null;

            JFedTrustStore trustStore = new JFedTrustStore(testbedInfoSource); //system trust store + certs in authority List
            trustStore.addTrustedPemCertificateIfNotAdded(targetServer.getCertificateChain());
            trustStore.addAllowedServerCertificateHostnameAlias(targetServer.getAllowedCertificateAlias());
            if (user.getUserAuthorityServer().getCertificateChain() != null)
                trustStore.addTrustedPemCertificateIfNotAdded(user.getUserAuthorityServer().getCertificateChain());

            assert injector != null;
            ConnectionBuilderFactory connectionBuilderFactory = injector.getInstance(ConnectionBuilderFactory.class);
            ConnectionBuilder connectionBuilder = connectionBuilderFactory.createConnectionBuilder();
            connectionBuilder.setDebugInfo(new JFedConnection.DebugInfo(targetService, user));
            connectionBuilder.setProxy(null, false);
            connectionBuilder.setUrl(serverUrlStr);
            connectionBuilder.useHttps(trustStore, null/*handleUntrustedCallback*/);
            connectionBuilder.useSslClientAuthentication(user.getClientCertificateChain(), user.getPrivateKey());
            con = connectionBuilder.buildSfaConnection();
        } else {
            try {
                baseCli.initTestTargetFromContextFileModule(line, out, err, in);
            } catch (BaseCli.CliArgumentException e) {
                LOG.debug("Got CliArgumentException", e);
                //User has been informed already
                return 1;
            }
            Injector injector = baseCli.getInjector(err);
            if (injector == null)
                return 1;

            TargetAuthority targetAuthority;
            GeniUser user;
            try {
                targetAuthority = injector.getInstance(TargetAuthority.class);
                user = injector.getInstance(GeniUser.class);
            } catch (AssertionError | Exception e) {
                baseCli.handleInjectException(e, err);
                return 1;
            }
            Server testedAuthorityToConnect = targetAuthority.getServerToConnect();

            assert api != null;
            URL serverUrl = ApiInfo.findUrl(testedAuthorityToConnect, api);
            if (serverUrl == null) {
                err.println("FATAL error: Authority " + testedAuthorityToConnect.getDefaultComponentManagerUrn() + " has no URL for " + apiArg+" (internal type = "+api+")");
                return 1;
            }
            assert serverUrl != null :
                    "Authority " + testedAuthorityToConnect.getDefaultComponentManagerUrn() + " has no URL for " + api;
            serverUrlStr = serverUrl.toString();

            if (!silent) {
                out.println("Read context properties from file \"" + line.getOptionValue("context-file") + "\":");
                out.println("   Tested Authority: " + testedAuthorityToConnect.getName());
                out.println("      URN: " + testedAuthorityToConnect.getDefaultComponentManagerUrn());
                out.println("      Hrn: " + testedAuthorityToConnect.getName());
                out.println("      Server certificates: " + testedAuthorityToConnect.getCertificateChain());
                out.println("      allowed server certificate hostname alias: " + testedAuthorityToConnect.getAllowedCertificateAlias());
                out.println("      URL for " + api + ": " + serverUrlStr);
                out.println("   User: " + user.getUserUrnString());
                out.println("      Authority URN: " + user.getUserAuthorityServer().getDefaultComponentManagerUrn());
            }

            con = injector.getInstance(JFedConnectionProvider.class).getConnectionByAuthority(user, testedAuthorityToConnect, api);
        }

        assert con != null;

        if (!silent)
            out.println("Connecting and executing " + command + "...");

        CommandLineCore.ExecuteMethodResult res = core.executeMethod(con, command, arguments, interactive);

        if (!silent && outputfile == null)
            out.println(apiArg + " " + command + " result: ");
        if (!silent && outputfile != null)
            out.println("Writing " + apiArg + " " + command + " result to file \"" + outputfile + "\"");

        if (outputfile == null)
            out.println(res.output);
        else {
            IOUtils.stringToFile(new File(outputfile), res.output);
        }

        if (!silent && debug) {
            out.println("DEBUG:");
            out.println("  Server URL: " + con.getServerUrl());
            if (con instanceof SfaConnection) {
                out.println("  HTTP sent: " + ((SfaConnection) con).getXmlRpcTransportFactory().getHttpSentHistory());
                out.println("  HTTP reply: " + ((SfaConnection) con).getXmlRpcTransportFactory().getHttpReceivedHistory());
            }
        }

        return res.exitvalue;
    }




}
