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

import be.iminds.ilabt.jfed.*;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.util.IOUtils;
import be.iminds.ilabt.jfed.util.KeyUtil;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.*;

public class BaseCli {
    private final static Logger LOG = LoggerFactory.getLogger(BaseCli.class);

    protected AbstractModule authoritiesModule;
    protected AbstractModule preferencesModule;
    protected AbstractModule userModule;
    protected AbstractModule testTargetModule;

    public BaseCli(final PrintStream err) {
        Thread.setDefaultUncaughtExceptionHandler(UncaughtCliExceptionLogger.getInstance(err));
    }

    public Injector getInjector(final PrintStream err, AbstractModule... extraModules) {
        assert userModule != null;
        assert authoritiesModule != null;
        if (userModule == null)
            throw new RuntimeException("Bug in CLI implementation: userModule was not initialised.");
        if (authoritiesModule == null)
            throw new RuntimeException("Bug in CLI implementation: authoritiesModule was not initialised.");
        if (preferencesModule == null) {
            preferencesModule = new CorePreferencesModule();
        }

        List<AbstractModule> mods = new ArrayList<>();
        mods.addAll(Arrays.asList(extraModules));
        mods.addAll(Arrays.asList(userModule, authoritiesModule, new PreLoginCoreModule(), new PostLoginCoreModule(), preferencesModule, new JFedWebApiClientModule()));

        //ParallelStitcherTest needs injector with HighLevelModule.
        // So if it is available, we will add it to the injector. (it should never hurt to add it if it is available -> it can if dep modules are not found!)
        String highLevelModuleClassname = "be.iminds.ilabt.jfed.highlevel.HighLevelModule";
        String sfaOnlyExperimentModuleClassname = "be.iminds.ilabt.jfed.highlevel.SfaOnlyExperimentModule";
        try {
            Class<? extends AbstractModule> highLevelModuleClass = (Class<? extends AbstractModule>) Class.forName(highLevelModuleClassname);
            AbstractModule highLevelModule = highLevelModuleClass.newInstance();
            
            Class<? extends AbstractModule> sfaOnlyExperimentModuleClass = (Class<? extends AbstractModule>) Class.forName(sfaOnlyExperimentModuleClassname);
            AbstractModule sfaOnlyExperimentModule = sfaOnlyExperimentModuleClass.newInstance();

            if (highLevelModule != null && sfaOnlyExperimentModule != null) {
                LOG.debug("Adding HighLevelModule to injector");
                mods.add(highLevelModule);
                LOG.debug("Adding SfaOnlyExperimentModule to injector");
                mods.add(sfaOnlyExperimentModule);
            }
        } catch (ClassNotFoundException e) {
            LOG.debug("HighLevelModule and/or SfaOnlyExperimentModule is not available: will not be added to injector");
        } catch (InstantiationException | IllegalAccessException e) {
            LOG.warn("Unexpected exception while loading HighLevelModule or SfaOnlyExperimentModule. Will try to continue without them.", e);
        }


        if (testTargetModule != null)
            mods.add(testTargetModule);
        try {
            Injector injector = Guice.createInjector(mods);
            return injector;
        } catch (AssertionError | Exception e) {
            handleInjectException(e, err);
            return null;
        }
    }

    public void handleInjectException(Throwable e, final PrintStream err) {
        Throwable curEx = e;
        while (curEx != null) {
            if (curEx instanceof BaseCli.CliArgumentException) {
                err.println("FATAL argument error: "+curEx.getMessage());
            }
            if (curEx instanceof ContextFile.ContextFileException) {
                err.println("FATAL context file error: "+curEx.getMessage());
            }
            curEx = curEx.getCause();
        }
        err.println("Fatal error (see message above), cannot continue.");
        LOG.error("Error creating injector", e);
    }

    public void initTestTargetFromContextFileModule(CommandLine line, final PrintStream out, final PrintStream err, final InputStream in) throws CliArgumentException {
        try {
            this.testTargetModule = BaseCli.getTestTargetFromContextFileModule(line, out, err, in);
        } catch (BaseCli.CliArgumentException e) {
            LOG.debug("Got CliArgumentException", e);
            throw e;
        } catch (FileNotFoundException e) {
            LOG.debug("Got FileNotFoundException", e);
            //User has been informed already
            throw new CliArgumentException("context file not found", e);
        } catch (IOException e) {
            LOG.debug("Got FileNotFoundException", e);
            //User has been informed already
            throw new CliArgumentException("Error reading context file", e);
        }
    }

    public void initAuthoritiesModule(CommandLine line) {
        String testbedsFilename = line.getOptionValue("authorities-file");

        BasicTestbedInfoModule basicTestbedInfoModule = new BasicTestbedInfoModule();
        if (testbedsFilename != null) {
            //old method
            //basicTestbedInfoModule.setAuthoritiesFile(new File(testAuthoritiesFilename));
            preferencesModule = new CorePreferencesModule(testbedsFilename);
        }
//        basicAuthoritiesModule.setFetchFromGeniClearingHouse(line.hasOption("clearinghouse")); //depracated
        this.authoritiesModule = basicTestbedInfoModule;
    }

    public void initUserModule(CommandLine line, final PrintStream out, final PrintStream err, final InputStream in) throws IOException, CliArgumentException {
        String pemKeyCertFilename = line.getOptionValue("cert-and-key-file");
        if (pemKeyCertFilename == null && line.hasOption("key-file")) {
            pemKeyCertFilename = line.getOptionValue("key-file");
        }
        String contextFilename = line.getOptionValue("context-file");

        //optional
        boolean interactive = line.hasOption("interactive");
        boolean silent = line.hasOption("silent");

        if (contextFilename != null && pemKeyCertFilename != null) {
            err.println("--context-file cannot be combined with -p (--cert-and-key-file)");
            throw new CliArgumentException("--context-file cannot be combined with -p (--cert-and-key-file)");
        }

        if (contextFilename == null && pemKeyCertFilename == null) {
            err.println("Either specify --cert-and-key-file (-p) or --context-file (-c) (exactly one).");
            throw new CliArgumentException("Either specify --cert-and-key-file (-p) or --context-file (-c) (exactly one).");
        }

        if (contextFilename == null) {
            boolean asked = false;

            Server usedServer = null;
            String pemKeyCert = null;

            if (pemKeyCertFilename == null) {
                File defaultPemKeyCertFile =
                        new File(System.getProperty("user.home") + File.separator + ".ssl" + File.separator + "geni_cert.pem");
                if (!interactive) {
                    err.println("No --context-file and no --key-file option specified, and no --interactive either: cannot get user info.");
                    throw new CliArgumentException("FATAL: No --context-file and no --key-file option specified, and no --interactive either: cannot get user info.");
                }
                pemKeyCertFilename =
                        IOUtils.askCommandLineInput("PEM key and certificate filename (default: \"" + defaultPemKeyCertFile.getPath() + "\")", in);
                asked = true;
                if (pemKeyCertFilename == null || pemKeyCertFilename.equals(""))
                    pemKeyCertFilename = defaultPemKeyCertFile.getPath();
            }

            if (asked) {
                if (!silent)
                    out.println("The commandline parameter for this user is:  -k " + pemKeyCertFilename);
            }

            String pem = IOUtils.fileToString(pemKeyCertFilename);
            boolean hasPassword = KeyUtil.isPemPrivateKeyEncrypted(pem);

            char[] pass = null;
            if (hasPassword) {
                if (line.hasOption("private-key-password")) {
                    pass = line.getOptionValue("private-key-password").toCharArray();
                } else {
                    if (!interactive) {
                        err.println("FATAL: Not in interactive mode, so password cannot be requested.");
                        throw new CliArgumentException("FATAL: Not in interactive mode, so password cannot be requested.");
                    }
                    if (hasPassword) {
                        pass = IOUtils.askCommandLinePassword("Private key password", in);
                    }
                }
            }

            this.userModule = new BaseCliKeyCertUserModule(pem, pass, new File(pemKeyCertFilename), new File(pemKeyCertFilename));
        } else {
            File contextFile = new File(contextFilename);
            if (!contextFile.exists())
                throw new FileNotFoundException("Cannot find Context properties file: " + contextFilename);

            Properties contextFileProperties = new Properties();
            contextFileProperties.load(new FileInputStream(contextFile));

            this.userModule = new BaseCliContextFileUserModule(contextFileProperties);
        }

        assert this.userModule != null;
    }

    /**
     * @return true if user is valid
     */
    public static boolean checkUser(GeniUser user, CommandLine line, final PrintStream out, final PrintStream err, final InputStream in) {
        //FEDIBBTDEV-878: check certificate validity
        List<X509Certificate> clientCertificateChain = user.getClientCertificateChain();
        int i = 0;
        for (X509Certificate c : clientCertificateChain) {
            try {
                c.checkValidity(new Date());
            } catch (CertificateExpiredException e) {
                err.println("\nFATAL: Certificate "+i+" (of "+clientCertificateChain.size()+") in the user " +
                        "certificate chain has expired. NotAfter="+c.getNotAfter()+" now="+new Date()+"\nCannot continue, exiting...");
                return false;
            } catch (CertificateNotYetValidException e) {
                err.println("\nFATAL: Certificate "+i+" (of "+clientCertificateChain.size()+") in the user " +
                        "certificate chain is not yet valid. NotBefore="+c.getNotBefore()+" now="+new Date()+"\nCannot continue, exiting...");
                return false;
            }
            i++;
        }
        return true;
    }


    //TODO: check if this alternative code to request password is better
//                File pemFile = new File(pemFilenameArg);
//                UserLoginModelManager loginModel = new UserLoginModelManager(testbedInfoSource, logger);
//                loginModel.setUserLoginModelType(UserLoginModelManager.UserLoginModelType.KEY_CERT_INTERNAL_INFO);
//                try {
//                    loginModel.getKeyCertUserLoginModel().setKeyCertPemFile(pemFile);
//                } catch (InvalidLoginException e) {
//                    System.err.println("Error occurred when reading certificate");
//                    e.printStackTrace();
//                }
//                if (loginModel.getKeyCertUserLoginModel().isPasswordRequired()) {
//                    boolean unlocked = false;
//                    if (privateKeyPassArg != null) {
//                        unlocked = loginModel.getKeyCertUserLoginModel().unlock(privateKeyPassArg);
//                        if (!unlocked) {
//                            err.println("Error: The password you provided on the command line is not correct.");
//                            System.setOut(orig_out);
//                            System.setErr(orig_err);
//                            System.setIn(orig_in);
//                            return 3;
//                        }
//                    } else
//                        while (!unlocked) {
//                            char[] password =
//                                    IOUtils.askCommandLinePassword("PEM file is locked. Please enter the passphrase", in);
//                            assert !new String(password).contains("\n");
//                            unlocked = loginModel.getKeyCertUserLoginModel().unlock(password);
//                            if (!unlocked) {
//                                out.println("Invalid passphrase! Try again...\n");
//                            }
//                        }
//                }
//                loginModel.login();
//                if (!loginModel.isUserLoggedIn()) {
//                    err.println("Not enough user data in pem file: \"" + pemFilenameArg + "\"");
//                    System.setOut(orig_out);
//                    System.setErr(orig_err);
//                    System.setIn(orig_in);
//                    return 2;
//                }
//                assert loginModel.getLoggedInGeniUser() != null;
//                assert loginModel.getUserAuthority() != null;
//
//                user = loginModel.getLoggedInGeniUser();



    public static void addUserConfig(Options options, String contextFileHelp, boolean addBackwardCompatibleOptions) {
        options.addOption(Option.builder("p").longOpt("cert-and-key-file")
                .desc("The file containing the user certificate and private key (both in PEM format)")
                .hasArg()
                .argName("PEM FILE")
                .build());
        if (addBackwardCompatibleOptions)
            options.addOption("k", "key-file", true, "alias for --cert-and-key-file   (Only present for backward compatibility. Might be removed in future versions)");
        options.addOption(Option.builder("c").longOpt("context-file")
                .desc(contextFileHelp)
                .hasArg()
                .argName("CONTEXT PROPERTIES FILE")
                .build());

        options.addOption(Option.builder("P").longOpt("private-key-password")
                .desc("The password of the private key. Only used if private key is password protected. Default: interactively ask password when needed.")
                .hasArg()
                .argName("CLEARTEXT PASSWORD")
                .build());

        //TODO add options to specify username and user authority if not in certificate
    }

    public static void addAuthoritiesConfig(Options options) {
        options.addOption(Option.builder().longOpt("authorities-file")
                .desc("The xml file containing the list of known authorities. Default: choose automatically")
                .hasArg()
                .argName("AUTHORITIES XML FILE")
                .build());
        options.addOption(Option.builder().longOpt("clearinghouse")
                .desc("Fetch certificates etc from geni clearinghouse first")
                .build());
    }

    public static TestTargetFromContextFileModule getTestTargetFromContextFileModule(
            CommandLine line,
            PrintStream out, PrintStream err, InputStream in)
            throws CliArgumentException, IOException {
        String testContextFilename = line.getOptionValue("context-file");
        if (testContextFilename == null || testContextFilename.isEmpty())
            throw new CliArgumentException("--context-file not specified");

        File testContextFile = new File(testContextFilename);
        if (!testContextFile.exists())
            throw new FileNotFoundException("Cannot find Context properties file: " + testContextFilename);

        Properties contextFileProperties = new Properties();
        contextFileProperties.load(new FileInputStream(testContextFile));

        return new TestTargetFromContextFileModule(contextFileProperties);
    }


    public static class CliArgumentException extends Exception {
        public CliArgumentException() {
        }

        public CliArgumentException(String message) {
            super(message);
        }

        public CliArgumentException(String message, Throwable cause) {
            super(message, cause);
        }

        public CliArgumentException(Throwable cause) {
            super(cause);
        }

        public CliArgumentException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    }
}
