package be.iminds.ilabt.jfed.experimenter_gui.config;

import be.iminds.ilabt.jfed.JFedWebApiClientModule;
import be.iminds.ilabt.jfed.experimenter_gui.util.ConnectivityDetector;
import be.iminds.ilabt.jfed.fedmon.webapi.client.FedmonWebApiClient;
import be.iminds.ilabt.jfed.fedmon.webapi.client.FedmonWebApiClientConfig;
import be.iminds.ilabt.jfed.fedmon.webapi.client.FedmonWebApiClientDirect;
import be.iminds.ilabt.jfed.fedmon.webapi.client.FedmonWebApiClientPropertiesConfig;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.authority.Fed4FireAuthorityList;
import be.iminds.ilabt.jfed.preferences.CorePreferenceKey;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.JFedTrustStore;
import be.iminds.ilabt.jfed.util.JFedUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.name.Named;
import io.dropwizard.jackson.Jackson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

/**
 * JFedGuiConfigProvider provides a JFedExperimenterGuiConfig using the JFedPreferences,
 * for use by the JFedGuiConfig implementation
 * <p>
 * This is similar to JFedTestbedInfoSource, but there is an important difference to note!
 * - JFedTestbedInfoSource directly provides a TestbedInfoSource
 * - JFedGuiConfigProvider does not directly provides a JFedGuiConfig (like JFedGuiConfigImpl)
 * instead, it provides a JFedExperimenterGuiConfig.
 * JFedGuiConfigImpl injects JFedGuiConfigProvider to get this JFedExperimenterGuiConfig and initialises using that.
 */
@Singleton
public class JFedExperimenterGuiConfigProvider implements Provider<JFedExperimenterGuiConfig> {
    private static final Logger LOG = LoggerFactory.getLogger(JFedExperimenterGuiConfigProvider.class);

    private final ConnectivityDetector connectivityDetector;
    private final URL webApiUrl;

    private static final ObjectMapper MAPPER = Jackson.newObjectMapper();

    /**
     * "AUTO" (= use online or fallback), "file:*" or "http*" or "BUILTIN" or "CACHE"
     */
    private final String userPreference;
    private final UserInfoProvider userInfoProvider;
    private final GeniUserProvider geniUserProvider;
    private final boolean saveToCache;
    private final be.iminds.ilabt.jfed.log.Logger logger;

    public static File getCacheDir() {
        String userDataDir = JFedUtils.getUserDataDirectory();
        if (userDataDir == null)
            return null;
        return new File(userDataDir, "guiConfigCache");
    }

    public static File getCacheFile(UserInfo userInfo) {
        File dir = getCacheDir();
        String suffix = "";
        if (userInfo.getExtra() != null && !userInfo.getExtra().isEmpty()) {
            suffix += "_E_";
            suffix += String.join("-", userInfo.getExtra());
        }
        if (userInfo.getUrn() != null) {
            if (!suffix.isEmpty()) suffix += "_";
            suffix += "_U_";
            GeniUrn urn = GeniUrn.parse(userInfo.getUrn());
            suffix += urn.getEncodedTopLevelAuthority() + "_" + urn.getEncodedResourceName();
        }
        return new File(dir, "userconfig_json_" + suffix + ".cache");
    }

    public static File getExtraGuiConfigDir() {
        String userDataDir = JFedUtils.getUserDataDirectory();
        if (userDataDir == null)
            return null;
        return new File(userDataDir, "extra_guiConfig");
    }

    public static File getOverwriteGuiConfigDir() {
        String userDataDir = JFedUtils.getUserDataDirectory();
        if (userDataDir == null)
            return null;
        return new File(userDataDir, "overwrite_guiConfig");
    }

    public static List<File> getExtraGuiConfigFiles() {
        File extraGuiConfigsDir = getExtraGuiConfigDir();
        if (!extraGuiConfigsDir.exists() || !extraGuiConfigsDir.isDirectory())
            return Collections.emptyList();
        File[] extraFiles = extraGuiConfigsDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".json"));
        return Arrays.asList(extraFiles);
    }

    public static List<File> getOverwriteGuiConfigFiles() {
        File overwriteGuiConfigsDir = getOverwriteGuiConfigDir();
        if (!overwriteGuiConfigsDir.exists() || !overwriteGuiConfigsDir.isDirectory())
            return Collections.emptyList();
        File[] overwriteFiles = overwriteGuiConfigsDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".json"));
        return Arrays.asList(overwriteFiles);
    }

    @Inject
    public JFedExperimenterGuiConfigProvider(
            GeniUserProvider geniUserProvider,
            JFedPreferences jFedPreferences,
            ConnectivityDetector connectivityDetector,
            UserInfoProvider userInfoProvider,
            @Named(JFedWebApiClientModule.NAMED__NO_CLIENT_AUTH_WEBAPI_URL) URL webApiUrl,
            be.iminds.ilabt.jfed.log.Logger logger) {
        this(geniUserProvider, jFedPreferences, connectivityDetector, userInfoProvider, webApiUrl, logger, true/*saveToCache*/);
    }
    public JFedExperimenterGuiConfigProvider(
            GeniUserProvider geniUserProvider,
            JFedPreferences jFedPreferences,
            ConnectivityDetector connectivityDetector,
            UserInfoProvider userInfoProvider,
            URL webApiUrl,
            be.iminds.ilabt.jfed.log.Logger logger,
            boolean saveToCache) {
        assert userInfoProvider != null;
        assert geniUserProvider != null;
        assert jFedPreferences != null;
        assert connectivityDetector != null;

        this.userInfoProvider = userInfoProvider;
        this.geniUserProvider = geniUserProvider;

        userPreference = jFedPreferences.getString(CorePreferenceKey.PREF_GUICONFIG_SOURCE);
        this.connectivityDetector = connectivityDetector;
        this.webApiUrl = webApiUrl;

        this.saveToCache = saveToCache;
        this.logger = logger;
    }

    private static JFedExperimenterGuiConfig getUsingUrl(URL url, UserInfo userInfo, be.iminds.ilabt.jfed.log.Logger logger) throws
            FedmonWebApiClient.FedmonWebApiClientException,
            Fed4FireAuthorityList.OfflineException {
        assert userInfo != null;

        Properties fedmonWebApiClientConfigProperties = new Properties();
        fedmonWebApiClientConfigProperties.setProperty("webapi_client_url_read_base", url.toExternalForm());
        fedmonWebApiClientConfigProperties.setProperty("webapi_client_url_write_base", "https://localhost/"); //no need ever to write

        JFedTrustStore bootstrapTrustStore = new JFedTrustStore();
        bootstrapTrustStore.addAllowedServerCertificateHostnameAlias(Fed4FireAuthorityList.FED4FIRE_TESTBED_DIRECTORY_SELFSIGNED_CERTIFICATE_ALLOWED_ALIAS);
        bootstrapTrustStore.addTrustedPemCertificateIfNotAdded(Fed4FireAuthorityList.FED4FIRE_TESTBED_DIRECTORY_SELFSIGNED_CERTIFICATE);
        FedmonWebApiClientConfig fedmonWebApiClientConfig = new FedmonWebApiClientPropertiesConfig(
                fedmonWebApiClientConfigProperties, bootstrapTrustStore, null/*userCertificate*/, null/*userPrivateKey*/,
                logger);

        FedmonWebApiClient fedmonWebApiClient = new FedmonWebApiClientDirect(fedmonWebApiClientConfig);
        JFedExperimenterGuiConfig res = fedmonWebApiClient.getJFedExperimenterGuiConfig(userInfo);

        //hack: server currently has a caching bug that causes userInfo to be for another user in some cases. So we overwrite userinfo here.
        res = new JFedExperimenterGuiConfigBuilder(res).setUser(userInfo).create();

        return res;
    }

    private static JFedExperimenterGuiConfig getFromFile(File file) throws IOException {
        if (!file.exists()) throw new FileNotFoundException("File \"" + file.getPath() + "\" not found");

        return getFromStream(new FileInputStream(file));
    }

    private static JFedExperimenterGuiConfig getFromStream(InputStream is) throws IOException {
        return MAPPER.readValue(is, JFedExperimenterGuiConfig.class);
    }

    private static boolean hasServer(Collection<Server> servers, Server target) {
        assert target != null;
        assert target.getId() != null;
        if (servers == null)
            return false;
        for (Server s : servers) {
            if (s.getId() != null &&
                    target.getId() != null &&
                    Objects.equals(s.getId(), target.getId()))
                return true;
        }
        return false;
    }

    private static ResourceClass getResourceClass(Collection<ResourceClass> rcs, String id) {
        assert id != null;
        if (rcs == null)
            return null;

        for (ResourceClass rc : rcs) {
            if (Objects.equals(rc.getId(), id))
                return rc;
        }
        return null;
    }

    private static void mergeConfig(JFedExperimenterGuiConfigBuilder target, JFedExperimenterGuiConfig toAdd, boolean overwrite) {
        assert target != null;
        assert toAdd != null;

        if (toAdd.getBindableServers() != null) {
            if (overwrite) {
                target.setBindableServers(toAdd.getBindableServers());
            } else {
                for (Server bindableServer : toAdd.getBindableServers()) {
                    if (!hasServer(target.getBindableServers(), bindableServer))
                        target.addBindableServer(bindableServer);
                }
            }
        }
        if (toAdd.getHideServers() != null) {
            if (overwrite) {
                target.setHideServers(toAdd.getHideServers());
            } else {
                for (Server hideServer : toAdd.getHideServers()) {
                    if (!hasServer(target.getHideServers(), hideServer))
                        target.addHideServer(hideServer);
                }
            }
        }

        if (toAdd.getGtsConfig() != null) {
            if (overwrite) {
                target.setGtsConfig(toAdd.getGtsConfig());
            } else {
                if (target.getGtsConfig() == null)
                    target.setGtsConfig(toAdd.getGtsConfig());
            }
        }

        if (toAdd.getFlags() != null) {
            for (Map.Entry<String, Object> e : toAdd.getFlags().entrySet()) {
                String key = e.getKey();
                Object val = e.getValue();
                if (overwrite) {
                    target.setFlag(key, val);
                } else {
                    if (!target.getFlags().containsKey(key))
                        target.setFlag(key, val);
                }
            }
        }

        if (toAdd.getResourceClasses() != null) {
            for (ResourceClass resourceClass : toAdd.getResourceClasses()) {
                assert resourceClass.getId() != null;
                ResourceClass existing = target.getResourceClasses() == null ? null :
                        getResourceClass(target.getResourceClasses(), resourceClass.getId());
                if (existing == null || overwrite) {
                    target.addResourceClass(resourceClass);
                }
            }
        }
    }

    private JFedExperimenterGuiConfig config = null;

    @Override
    public JFedExperimenterGuiConfig get() {
        if (!geniUserProvider.isUserLoggedIn())
            throw new RuntimeException("Cannot work without user logged in");
        final GeniUrn userUrn = geniUserProvider.getLoggedInGeniUser().getUserUrn();
        assert userUrn != null;

        if (config == null ||
                (config.getUser() != null && userUrn != null && config.getUser().getUrn() != null && !Objects.equals(userUrn.toString(), config.getUser().getUrn()))
                ) {
            config = initConfig(userInfoProvider.createUserInfo(userUrn), userPreference, connectivityDetector, webApiUrl, logger, saveToCache);
        }
        return config;
    }

    private final ExecutorService prefetchExecutorService =
            Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("Prefetch-UserConfig-%d").build());

    private ConfigPrefetcher currentConfigPrefetcher = null;

    public class ConfigPrefetcher extends FutureTask<JFedExperimenterGuiConfig> {
        private final UserInfo userInfo;

        private ConfigPrefetcher(UserInfo userInfo) {
            super(new Callable<JFedExperimenterGuiConfig>() {
                @Override
                public JFedExperimenterGuiConfig call() throws Exception {
                    if (config == null || !Objects.equals(userInfo, config.getUser()))
                        config = initConfig(userInfo, userPreference, connectivityDetector, webApiUrl, logger, saveToCache);
                    return config;
                }
            });
            this.userInfo = userInfo;
        }

        public UserInfo getUserInfo() {
            return userInfo;
        }
    }

    public ConfigPrefetcher prefetchConfig(UserInfo userInfo) {
        if (currentConfigPrefetcher != null && Objects.equals(currentConfigPrefetcher.getUserInfo(), userInfo))
            return currentConfigPrefetcher;
        else {
            if (currentConfigPrefetcher != null)
                currentConfigPrefetcher.cancel(true);

            currentConfigPrefetcher = new ConfigPrefetcher(userInfo);
            prefetchExecutorService.execute(currentConfigPrefetcher);

            return currentConfigPrefetcher;
        }
    }

    @Nullable
    private static JFedExperimenterGuiConfig initConfig(UserInfo userInfo, String userPreference, ConnectivityDetector connectivityDetector, URL webApiUrl,
                                                        be.iminds.ilabt.jfed.log.Logger logger, boolean saveToCache) {
        JFedExperimenterGuiConfigBuilder resBuilder = null;

        String altUrl = null;
        String filename = null;
        boolean builtinOnly = false;
        boolean cacheOnly = false;

        if (userPreference != null) {
            String testbedsSourcePref = userPreference;
            if (!testbedsSourcePref.equalsIgnoreCase("AUTO") && !testbedsSourcePref.equalsIgnoreCase("DEFAULT")) {
                LOG.debug("Skipping default guiConfig method, and using user specified method: \"" + userPreference + "\"");
                if (testbedsSourcePref.equalsIgnoreCase("BUILTIN")) {
                    builtinOnly = true;
                } else {
                    if (testbedsSourcePref.equalsIgnoreCase("CACHE")) {
                        //todo: support locally cached version
                        cacheOnly = true;
                    } else {
                        if (testbedsSourcePref.startsWith("file:")) {
                            filename = testbedsSourcePref.substring(5);
                        } else {
                            //URL
                            altUrl = testbedsSourcePref;
                        }
                    }
                }
            }  //else: nothing to do, we just go through the normal procedure below else
        }

        boolean tryFile = !builtinOnly && !cacheOnly && filename != null;
        boolean tryUrl = !builtinOnly && !cacheOnly;
        boolean tryBuiltIn = true;
        boolean tryCache = !builtinOnly;

        if (tryFile) {
            File userFile = new File(filename);
            try {
                resBuilder = new JFedExperimenterGuiConfigBuilder(getFromFile(userFile));
                LOG.debug("Successfully read guiConfig at " + userFile);
            } catch (AssertionError | Exception e) {
                LOG.error("Something went wrong while trying to read guiConfig at " + userFile, e);
                resBuilder = null;
            }
        }

        if (resBuilder == null && tryUrl) {
            boolean urlSuccess = false;
            if (!connectivityDetector.isNoInternet()) {
                try {
                    URL url;
                    try {
                        if (altUrl != null)
                            url = new URL(altUrl);
                        else
                            url = webApiUrl;

                        resBuilder = new JFedExperimenterGuiConfigBuilder(getUsingUrl(url, userInfo, logger));
                        urlSuccess = true;
                        LOG.debug("Successfully read guiConfig from " + url.toExternalForm());
                    } catch (MalformedURLException e) {
                        LOG.error("Invalid URL for testbedInfoSource", e);
                        urlSuccess = false;
                    }
                } catch (AssertionError e) {
                    LOG.error("Something went wrong while trying to fetch Fed4Fire guiConfig: ", e);
                    urlSuccess = false;
                    resBuilder = null;
                } catch (Fed4FireAuthorityList.OfflineException e) {
                    LOG.error("There is not internet connectivity", e);
                    urlSuccess = false;
                    resBuilder = null;
                    connectivityDetector.setNoInternet(true);
                } catch (Exception e) {
                    LOG.error("Something went wrong while trying to fetch Fed4Fire guiConfig: ", e);
                    urlSuccess = false;
                    resBuilder = null;
                }
                if (!urlSuccess) {
                    if (altUrl == null)
                        LOG.warn("Something went wrong trying to load the default guiConfig URL");
                    else
                        LOG.warn("Something went wrong trying to load guiConfig from " + altUrl);

                    LOG.debug("Will use cache and builtin as backup");
                    tryBuiltIn = true;
                    tryCache = true;
                }
            } else {
                LOG.debug("Not trying to get guiConfig from URL, because connectivityDetector detected no internet connection");
            }
        }

        File cachedGuiConfigFile = getCacheFile(userInfo);
        if (resBuilder == null && tryCache) {
            if (cachedGuiConfigFile != null && cachedGuiConfigFile.exists()) {
                try {
                    resBuilder = new JFedExperimenterGuiConfigBuilder(getFromFile(cachedGuiConfigFile));
                    LOG.debug("Successfully read guiConfig from cache at " + cachedGuiConfigFile);
                } catch (AssertionError | Exception e) {
                    LOG.error("Something went wrong while trying to read guiConfig at " + cachedGuiConfigFile, e);
                    resBuilder = null;
                }
            } else {
                LOG.debug("No cached guiConfig file. " + cachedGuiConfigFile);
            }
        }

        if (resBuilder == null && tryBuiltIn) {
            try {
                String suffix = userInfo.getUrn() == null ? ""
                        : "_" + GeniUrn.parse(userInfo.getUrn()).getEncodedTopLevelAuthority_withoutSubAuth();
                InputStream is =
                        JFedExperimenterGuiConfigProvider.class.getResourceAsStream("defaultGuiConfig" + suffix + ".json");
                resBuilder = new JFedExperimenterGuiConfigBuilder(getFromStream(is));
            } catch (AssertionError | Exception e) {
                LOG.error("Something went wrong while using Builtin guiConfig: ", e);
                resBuilder = null;
            }

            if (resBuilder == null) {
                try {
                    InputStream is =
                            JFedExperimenterGuiConfigProvider.class.getResourceAsStream("defaultGuiConfig.json");
                    resBuilder = new JFedExperimenterGuiConfigBuilder(getFromStream(is));
                } catch (AssertionError | Exception e) {
                    LOG.error("Something went wrong while using Builtin guiConfig: ", e);
                    resBuilder = null;
                }
            }

            if (resBuilder == null) {
                LOG.error("No builtin guiConfig could be read.");
            } else {
                LOG.info("Successfully read a builtin guiConfig");
            }
        }

        //finally, check the extra an overwrite dirs
        if (resBuilder != null) {
            for (File extraFile : getExtraGuiConfigFiles()) {
                try {
                    JFedExperimenterGuiConfig extraGuiConfig = getFromFile(extraFile);
                    if (extraGuiConfig != null) {
                        mergeConfig(resBuilder, extraGuiConfig, false/*overwrite*/);
                    }
                } catch (AssertionError | Exception e) {
                    LOG.error("Something went wrong while trying to read extra guiConfig from " + extraFile, e);
                }
            }
            for (File overwriteFile : getOverwriteGuiConfigFiles()) {
                try {
                    JFedExperimenterGuiConfig overwriteGuiConfig = getFromFile(overwriteFile);
                    if (overwriteGuiConfig != null) {
                        mergeConfig(resBuilder, overwriteGuiConfig, true/*overwrite*/);
                    }
                } catch (AssertionError | Exception e) {
                    LOG.error("Something went wrong while trying to read overwrite guiConfig from " + overwriteFile, e);
                }
            }
        } else {
            LOG.warn("Will ignore extra and overwrite guiConfigs, because no base guiConfig could be found.");
        }

        if (resBuilder != null) {
            JFedExperimenterGuiConfig config = resBuilder.create();

            LOG.info("jFed initialised GuiConfig with " + (config.getResourceClasses() == null ? "null" : config.getResourceClasses().size()) + " resourceClasses.");

            //before successful return, try caching it. This may run in the background.

            if (saveToCache)
                saveCacheInBackground(config, cachedGuiConfigFile);

            return config;
        } else {
            LOG.error("No method to get testbed info worked!");
            return null;
        }
    }


    private static void saveCacheInBackground(final JFedExperimenterGuiConfig guiConfig, final File targetFile) {
        Runnable r = () -> {
            try {
                File cacheDir = getCacheDir();
                if (!cacheDir.exists())
                    //noinspection ResultOfMethodCallIgnored
                    cacheDir.mkdir();
                save(guiConfig, targetFile);
            } catch (AssertionError | Exception e) {
                LOG.error("Something went wrong while trying to write testbeds to cache " + targetFile, e);
            }
        };
        new Thread(null, r, "UserConfigCacheSaver").start();
    }

    public static void save(JFedExperimenterGuiConfig guiConfig, File targetFile) throws IOException {
        LOG.info("Saving jFed guiConfig to file \"" + targetFile + "\"");

        //first see if the dir exists, and try to create it if not.
        File dir = targetFile.getParentFile();
        if (!dir.exists()) //noinspection ResultOfMethodCallIgnored
            dir.mkdir();
        if (!dir.exists()) {
            LOG.warn("could not save authorities to \"" + targetFile.getName() + "\", as dir \"" + dir.getName() + "\" could not be created.");
            return;
        }

        MAPPER.writer().with(SerializationFeature.INDENT_OUTPUT).writeValue(targetFile, guiConfig);
    }
}
