package be.iminds.ilabt.jfed.preferences;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
import be.iminds.ilabt.jfed.lowlevel.ApiInfo;
import be.iminds.ilabt.jfed.lowlevel.RetrySettings;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.JFedUtils;
import be.iminds.ilabt.jfed.util.TextUtil;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * JFedCorePreferences
 */
public class JFedCorePreferences implements JFedPreferences {
    private final static Logger LOG = LoggerFactory.getLogger(JFedCorePreferences.class);
    protected final File propertiesFile;
    protected final PropertiesConfiguration config;

    public JFedCorePreferences() {
        this.propertiesFile = JFedUtils.createPropertiesFile("experimenter-ssh.properties");

        if (propertiesFile != null) {
            try {
                config = new PropertiesConfiguration(propertiesFile);
            } catch (ConfigurationException e) {
                LOG.error("Could not create PropertiesConfiguration", e);
                throw new RuntimeException("Could not create PropertiesConfiguration", e);
            }
        } else {
            LOG.error("Fallback: Settings will NOT be saved!");
            config = new PropertiesConfiguration();
        }

        //no autosave! config.setAutoSave(true);
        config.setThrowExceptionOnMissing(false);

        //use defaults to fill in blanks
        fillinBlanks(config);
    }

    public void fillinBlanks() {
        fillinBlanks(config);
    }

    protected void fillinBlanks(PropertiesConfiguration config) {
        if (!config.containsKey(CorePreferenceKey.PREF_TESTBEDS_JSON_SOURCE.key))
            config.setProperty(CorePreferenceKey.PREF_TESTBEDS_JSON_SOURCE.key, "AUTO");

        if (!config.containsKey(CorePreferenceKey.PREF_GUICONFIG_SOURCE.key))
            config.setProperty(CorePreferenceKey.PREF_GUICONFIG_SOURCE.key, "AUTO");

        if (!config.containsKey(CorePreferenceKey.PREF_DEBUG_CPU.key))
            config.setProperty(CorePreferenceKey.PREF_DEBUG_CPU.key, false);

        if (!config.containsKey(CorePreferenceKey.PREF_LOGGING_OVERWRITE.key))
            config.setProperty(CorePreferenceKey.PREF_LOGGING_OVERWRITE.key, false);
        if (!config.containsKey(CorePreferenceKey.PREF_LOGGING_ROOT_LEVEL.key))
            config.setProperty(CorePreferenceKey.PREF_LOGGING_ROOT_LEVEL.key, "INFO");
//        if (!config.containsKey(CorePreferenceKey.PREF_LOGGING_APPENDER_BUGREPORT_LEVEL.key))
//            config.setProperty(CorePreferenceKey.PREF_LOGGING_APPENDER_BUGREPORT_LEVEL.key, "INFO");
//        if (!config.containsKey(CorePreferenceKey.PREF_LOGGING_APPENDER_STDOUT_LEVEL.key))
//            config.setProperty(CorePreferenceKey.PREF_LOGGING_APPENDER_STDOUT_LEVEL.key, "INFO");
        if (!config.containsKey(CorePreferenceKey.PREF_LOGGING_EXTTERM_LEVEL.key))
            config.setProperty(CorePreferenceKey.PREF_LOGGING_EXTTERM_LEVEL.key, "DEBUG");

        if (!config.containsKey(CorePreferenceKey.PREF_TESTBEDSPECIFIC_EXOGENI_FORCE_EXOSM.key))
            config.setProperty(CorePreferenceKey.PREF_TESTBEDSPECIFIC_EXOGENI_FORCE_EXOSM.key, "AUTO");

        if (!config.containsKey(CorePreferenceKey.PREF_TESTBEDSPECIFIC_EXOGENI_RECOVER_METHOD.key))
            config.setProperty(CorePreferenceKey.PREF_TESTBEDSPECIFIC_EXOGENI_RECOVER_METHOD.key, "BOTH");

        if (!config.containsKey(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED.key))
            config.setProperty(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED.key, ProxyPreferencesManager.NEVER_USE_PROXY);

        if (!config.containsKey(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH.key))
            config.setProperty(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH.key, ProxyPreferencesManager.NEVER_USE_PROXY);

        if (!config.containsKey(CorePreferenceKey.PREF_SSHPROXY_DNS_OVER_PROXY_JFED.key))
            config.setProperty(CorePreferenceKey.PREF_SSHPROXY_DNS_OVER_PROXY_JFED.key, false);

        if (!config.containsKey(CorePreferenceKey.PREF_SSHPROXY_DNS_OVER_PROXY_SSH.key))
            config.setProperty(CorePreferenceKey.PREF_SSHPROXY_DNS_OVER_PROXY_JFED.key, false);

        if (getRetrySettings() == null)
            setRetrySettings(new RetrySettings()/* = use internal default values*/);
    }

    public boolean containsPreference(PreferenceKey key) {
        return config.containsKey(key.getKey());
    }

    public boolean hasKey(PreferenceKey key) {
        return config.containsKey(key.getKey());
    }

    public String getString(PreferenceKey key, String defaultValue) {
        if (!config.containsKey(key.getKey())) return defaultValue;
        else
            return getString(key);
    }

    @Override
    public String getString(PreferenceKey key) {
        if (!config.containsKey(key.getKey())) return null;

        Object val = config.getProperty(key.getKey());
        if (val == null) {
            LOG.warn("BUG in JFedCorePreferences: existing preference for \"" + key.getKey() + "\" returned null. Using fallback to null.");
            return null;
        }
        if (val instanceof String)
            return (String) val;

        LOG.debug("Preference \"" + key.getKey() + "\" is of type " + val.getClass().getName() + " instead of String. Will convert it to String.");
        return val + "";
    }

    @Override
    @Nonnull
    public List<String> getStringList(PreferenceKey key) {
        if (!config.containsKey(key.getKey())) return Collections.emptyList();

        Object val = config.getProperty(key.getKey());
        if (val == null) {
            LOG.warn("BUG in JFedCorePreferences: existing preference for \"" + key.getKey() + "\" returned null. Using fallback to empty list.");
            return Collections.emptyList();
        }
        if (val instanceof String) {
            return Collections.singletonList((String) val);
        }
        if (val instanceof List) {
            return (List) val;
        }

        LOG.debug("Preference \"" + key.getKey() + "\" is of type " + val.getClass().getName() + " instead of String or String List. Will convert it to List with one String.");
        return Collections.singletonList(val + "");
    }

    @Override
    public void setString(PreferenceKey key, String value) {
        config.setProperty(key.getKey(), value);
    }
    @Override
    public void setStringList(PreferenceKey key, @Nonnull List<String> value) {
        config.setProperty(key.getKey(), value.stream().collect(Collectors.joining(",")));
    }

    public Integer getInt(PreferenceKey key) {
        if (!config.containsKey(key.getKey())) return null;
        return config.getInt(key.getKey());
    }

    public Integer getInt(PreferenceKey key, int defaultValue) {
        if (!config.containsKey(key.getKey())) return defaultValue;
        return config.getInt(key.getKey());
    }

    public void setInt(PreferenceKey key, Integer value) {
        config.setProperty(key.getKey(), value);
    }

    public Boolean getBoolean(PreferenceKey key) {
        if (!config.containsKey(key.getKey())) return null;

        Object val = config.getProperty(key.getKey());
        assert val != null;

        Boolean res = TextUtil.objectToBoolean(val);
        if (res != null)
            return res;
        else
            throw new IllegalStateException("Stored preference for " + key.getKey() + " is null. This is not a valid boolean value.");
    }

    public boolean getBoolean(PreferenceKey key, boolean defaultValue) {
        if (!config.containsKey(key.getKey())) return defaultValue;
        Boolean res = getBoolean(key);
        assert res != null;
        return res;
    }

    public void setBoolean(PreferenceKey key, Boolean value) {
        config.setProperty(key.getKey(), value);
    }

    public File getFile(PreferenceKey key) {
        if (!config.containsKey(key.getKey())) return null;
        return new File(config.getString(key.getKey()));
    }

    public void setFile(PreferenceKey key, File value) {
        config.setProperty(key.getKey(), value.getPath());
    }

    public void clear(PreferenceKey key) {
        config.clearProperty(key.getKey());
    }

    public File getPropertiesFile() {
        return propertiesFile;
    }


    @Override
    public void addPreferenceChangeListener(PreferenceKey prefDebugCpu, PreferenceChangeListener preferenceChangeListener) {
        //TODO: implement
    }


//  no longer needed: AuthorityFinder always checks the current preferences
//    public void updateForceExoSM(TestbedInfoSource testbedInfoSource) {


    @Override
    public void setRetrySettings(RetrySettings retrySettings) {
        setInt(CorePreferenceKey.RETRY_TIMEOUT_MAX, retrySettings.getTimeoutMaxRetries());
        setInt(CorePreferenceKey.RETRY_BUSY_MAX, retrySettings.getBusyMaxRetries());
        setInt(CorePreferenceKey.RETRY_POSTEMPERR_MAX, retrySettings.getPossibleTemporaryErrorMaxRetries());

        setString(CorePreferenceKey.RETRY_TIMEOUT_MS_DELAY, retrySettings.getTimeoutMsBeforeRetry() + "");
        setString(CorePreferenceKey.RETRY_BUSY_MS_DELAY, retrySettings.getBusyMsBeforeRetry() + "");
        setString(CorePreferenceKey.RETRY_POSTEMPERR_MS_DELAY, retrySettings.getPossibleTemporaryErrorMsBeforeRetry() + "");
    }

    @Nonnull
    @Override
    public RetrySettings getRetrySettings() {
        Integer timeoutMaxRetries =
                getInt(CorePreferenceKey.RETRY_TIMEOUT_MAX, RetrySettings.DEFAULT_TIMEOUT_MAX_RETRIES);
        Integer busyMaxRetries = getInt(CorePreferenceKey.RETRY_BUSY_MAX, RetrySettings.DEFAULT_BUSY_MAX_RETRIES);
        Integer possibleTemporaryErrorMaxRetries =
                getInt(CorePreferenceKey.RETRY_POSTEMPERR_MAX, RetrySettings.DEFAULT_TIMEOUT_MAX_RETRIES);

        String timeoutMsBeforeRetry = getString(CorePreferenceKey.RETRY_TIMEOUT_MS_DELAY,
                Integer.toString(RetrySettings.DEFAULT_TIMEOUT_MS_BEFORE_RETRY));
        String busyMsBeforeRetry = getString(CorePreferenceKey.RETRY_BUSY_MS_DELAY,
                Integer.toString(RetrySettings.DEFAULT_BUSY_MS_BEFORE_RETRY));
        String possibleTemporaryErrorMsBeforeRetry = getString(CorePreferenceKey.RETRY_POSTEMPERR_MS_DELAY,
                Integer.toString(RetrySettings.DEFAULT_TIMEOUT_MS_BEFORE_RETRY));

        try {
            return new RetrySettings(
                    timeoutMaxRetries, Long.parseLong(timeoutMsBeforeRetry),
                    busyMaxRetries, Long.parseLong(busyMsBeforeRetry),
                    possibleTemporaryErrorMaxRetries, Long.parseLong(possibleTemporaryErrorMsBeforeRetry)
            );
        } catch (NumberFormatException e) {
            LOG.error("Error parsing a RetrySettings", e);
            return new RetrySettings();
        }
    }


    public void setScs(Server server) {
        if (server == null) {
            clear(CorePreferenceKey.PREF_SCS_URN);
            clear(CorePreferenceKey.PREF_SCS_URL);
            return;
        }
        Service service = ApiInfo.findService(server, ApiInfo.ApiName.GENI_SCS, 1);
        if (service == null)
            throw new RuntimeException("Authority " + server.getDefaultComponentManagerUrn() + " has no SCS service");
        URL u;
        try {
            u = new URL(service.getUrl());
        } catch (MalformedURLException e) {
            throw new RuntimeException("Authority " + server.getDefaultComponentManagerUrn() + " has invalid SCS Url \"" + service.getUrl() + "\"", e);
        }
        try {
            setScs(new GeniUrn(service.getUrn()), u);
        } catch (GeniUrn.GeniUrnParseException e) {
            throw new RuntimeException("Authority " + server.getDefaultComponentManagerUrn() + " has invalid SCS Urn \"" + service.getUrn() + "\"");
        }
    }

    public void setScs(GeniUrn scsUrn, URL url) {
        if (scsUrn == null && url == null) {
            clear(CorePreferenceKey.PREF_SCS_URN);
            clear(CorePreferenceKey.PREF_SCS_URL);
            return;
        }
        if (scsUrn != null)
            setString(CorePreferenceKey.PREF_SCS_URN, scsUrn.toString());
        else
            clear(CorePreferenceKey.PREF_SCS_URN);
        setString(CorePreferenceKey.PREF_SCS_URL, url.toString());
    }

    public void setScs(URL url) {
        if (url == null) {
            clear(CorePreferenceKey.PREF_SCS_URN);
            clear(CorePreferenceKey.PREF_SCS_URL);
            return;
        }
        clear(CorePreferenceKey.PREF_SCS_URN);
        setString(CorePreferenceKey.PREF_SCS_URL, url.toString());
    }

    public URL getScsUrl() {
        String u = getString(CorePreferenceKey.PREF_SCS_URL);
        if (u == null) return null;
        try {
            return new URL(u);
        } catch (MalformedURLException e) {
            clear(CorePreferenceKey.PREF_SCS_URL);
            throw new RuntimeException("Stored SCS URL is not valid URL (and has been removed from config). value=\"" + u + "\"");
        }
    }

    public GeniUrn getScsUrn() {
        String u = getString(CorePreferenceKey.PREF_SCS_URN);
        if (u == null) return null;
        try {
            return new GeniUrn(u);
        } catch (GeniUrn.GeniUrnParseException e) {
            clear(CorePreferenceKey.PREF_SCS_URN);
            throw new RuntimeException("Stored SCS URN is not valid URN (and has been removed from config). value=\"" + u + "\"");
        }
    }

    public Server getScsAuthority(TestbedInfoSource testbedInfoSource) {
        URL scsUrl = getScsUrl();
        String scsUrnString = getString(CorePreferenceKey.PREF_SCS_URN);
        GeniUrn scsUrn = scsUrnString == null ? null : GeniUrn.parse(scsUrnString);

        Server scsServer = null;
        if (scsUrn != null) {
            scsServer = testbedInfoSource.getByUrnExact(scsUrn);
            Service scsService = ApiInfo.findService(scsServer, ApiInfo.ApiName.GENI_SCS, 1);
            assert scsServer == null || (scsService != null && Objects.equals(scsService.getUrl(), scsUrl.toExternalForm()));
        }
        if (scsServer == null && scsUrl != null)
            scsServer = testbedInfoSource.getServerByUrl(scsUrl);
        if (scsServer == null && scsUrl != null) {
            TestbedBuilder testbedBuilder = new TestbedBuilder();
            ServerBuilder serverBuilder = new ServerBuilder();
            ServiceBuilder serviceBuilder = new ServiceBuilder();

            testbedBuilder.addServerBuilder(serverBuilder);
            serverBuilder.addServiceBuilder(serviceBuilder);

            testbedBuilder.setLongName("Config specified SCS");
            serverBuilder.setName("Config specified SCS");

            testbedBuilder.setDefaultComponentManagerUrn(scsUrn != null ? scsUrn.toString() : null);
            serverBuilder.setDefaultComponentManagerUrn(scsUrn.toString());

            serviceBuilder.setUrn(scsUrn.toString());
            serviceBuilder.setUrl(scsUrl.toExternalForm());
            serviceBuilder.setApi(ApiInfo.ApiName.GENI_SCS.getId());
            serviceBuilder.setApiVersion("1");

            Testbed scsTestbed = testbedBuilder.create();
            scsServer = scsTestbed.getServers().get(0);
            assert scsServer != null;
        }

        return scsServer;
    }


}
