package be.iminds.ilabt.jfed.lowlevel.authority;

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.BasicTestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.preferences.CorePreferenceKey;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.JFedTrustStore;
import be.iminds.ilabt.jfed.util.JFedUtils;
import com.google.inject.name.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Provider;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

/**
 * jFedCombinedAuthorityList
 * <p/>
 * This combines authority info from various sources, to create the list that is used in all jFed tools.
 */
public class JFedTestbedInfoSource implements Provider<TestbedInfoSource> {
    private static final Logger LOG = LoggerFactory.getLogger(JFedTestbedInfoSource.class);
    private static final File cachedTestbedsFile = getCachedTestbedsFile();
    private static final File extraTestbedsDir = getExtraTestbedsDir();
    private static final File overwriteTestbedsDir = getOverwriteTestbedsDir();

    /**
     * "AUTO" (= use online f4f testbeds.json or fallback), "file:*" or "http*" or "BUILTIN" or "CACHE"
     */
    private final String userPreference;

    private final ConnectivityDetector connectivityDetector;
    private final URL webApiUrl;
    private final be.iminds.ilabt.jfed.log.Logger logger;

    @Inject
    public JFedTestbedInfoSource(JFedPreferences jFedPreferences,
                                 ConnectivityDetector connectivityDetector,
                                 @Named(JFedWebApiClientModule.NAMED__NO_CLIENT_AUTH_WEBAPI_URL) URL webApiUrl,
                                 be.iminds.ilabt.jfed.log.Logger logger) {
        userPreference = jFedPreferences.getString(CorePreferenceKey.PREF_TESTBEDS_JSON_SOURCE);
        this.connectivityDetector = connectivityDetector;
        this.webApiUrl = webApiUrl;
        this.logger = logger;
    }

    public static File getCachedTestbedsFile() {
        String userDataDir = JFedUtils.getUserDataDirectory();
        if (userDataDir == null)
            return null;
        return new File(userDataDir, "testbeds_cache.json");
    }
    public static File getExtraTestbedsDir() {
        String userDataDir = JFedUtils.getUserDataDirectory();
        if (userDataDir == null)
            return null;
        return new File(userDataDir, "extra_testbeds");
    }
    public static File getOverwriteTestbedsDir() {
        String userDataDir = JFedUtils.getUserDataDirectory();
        if (userDataDir == null)
            return null;
        return new File(userDataDir, "overwrite_testbeds");
    }

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


    private List<Testbed> getTestbedsUsingUrl(URL url) throws Exception {
        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);
        List<Testbed> testbeds = null;
        try {
            testbeds = fedmonWebApiClient.getAll(Testbed.class, true/*embedWithLinkedObjects*/);
        } catch (FedmonWebApiClient.FedmonWebApiClientException e) {
            throw new RuntimeException("Failed to fetch Testbed info from server", e);
            //TODO generate Fed4FireAuthorityList.OfflineException when needed
        }
        return testbeds;
    }

    @Override
    public TestbedInfoSource get() {
        List<Testbed> testbeds = 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")) {
                //nothing to do, we just go through the normal procedure below
            } else {
                LOG.debug("Skipping default testbedInfo 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 {
                            altUrl = testbedsSourcePref;
                        }
                    }
                }
            }
        }

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

        if (testbeds == null && tryFile) {
            File userFile = new File(filename);
            try {
                testbeds = StoredTestbedInfo.load(userFile);
                if (testbeds == null) {
                    LOG.error("Something went wrong while trying to read testbedInfo at " + userFile);
                    testbeds = null;
                } else {
                    LOG.debug("Successfully read testbedInfo at " + userFile);
                }
            } catch (AssertionError | Exception e) {
                LOG.error("Something went wrong while trying to read testbedInfo at " + userFile, e);
                testbeds = null;
            }
        }

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

                        testbeds = getTestbedsUsingUrl(url);
                        urlSuccess = testbeds != null;
                    } 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 testbedInfo: ", e);
                    urlSuccess = false;
                    testbeds = null;
                } catch (Fed4FireAuthorityList.OfflineException e) {
                    LOG.error("There is not internet connectivity", e);
                    urlSuccess = false;
                    testbeds = null;
                    connectivityDetector.setNoInternet(true);
                } catch (Exception e) {
                    LOG.error("Something went wrong while trying to fetch Fed4Fire testbedInfo: ", e);
                    urlSuccess = false;
                    testbeds = null;
                }
                if (!urlSuccess) {
                    if (altUrl == null)
                        LOG.warn("Something went wrong trying to load the default testbedInfo URL");
                    else
                        LOG.warn("Something went wrong trying to load testbedInfo from " + altUrl);

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

        if (testbeds == null && tryCache) {
            if (cachedTestbedsFile != null && cachedTestbedsFile.exists()) {
                try {
                    testbeds = StoredTestbedInfo.load(cachedTestbedsFile);
                    if (testbeds == null) {
                        LOG.error("Something went wrong while trying to read testbedInfo at " + cachedTestbedsFile);
                        testbeds = null;
                    } else {
                        LOG.debug("Successfully read testbedInfo from cache at " + cachedTestbedsFile);
                    }
                } catch (AssertionError | Exception e) {
                    LOG.error("Something went wrong while trying to read testbedInfo at " + cachedTestbedsFile, e);
                    testbeds = null;
                }
            } else {
                LOG.debug("No cached testbeds file. "+cachedTestbedsFile);
            }
        }

        if (testbeds == null && tryBuiltIn) {
            try {
                InputStream is = JFedTestbedInfoSource.class.getResourceAsStream("testbeds.json");
                testbeds = StoredTestbedInfo.load(is);
            } catch (AssertionError | Exception e) {
                LOG.error("Something went wrong while using Builtin testbeds: ", e);
                testbeds = null;
            }
        }

        //finally, check the extra an overwrite dirs
        if (testbeds != null) {
            for (File extraFile : getExtraTestbedFiles()) {
                try {
                    List<Testbed> extraTestbeds = StoredTestbedInfo.load(extraFile);
                    if (extraTestbeds != null) {
                        for (Testbed et : extraTestbeds) {
                            boolean exists = false;
                            for (Testbed existing : testbeds) {
                                if (Objects.equals(existing.getId(), et.getId())) {
                                    LOG.debug("A testbed with ID \"" + et.getId() + "\" is already in the list. Will ignore the one in extra file " + extraFile.getName());
                                    exists = true;
                                }
                                if (existing.getDefaultComponentManagerUrn() != null &&
                                        et.getDefaultComponentManagerUrn() != null &&
                                        Objects.equals(existing.getDefaultComponentManagerUrn(), et.getDefaultComponentManagerUrn())) {
                                    exists = true;
                                    LOG.debug("A testbed with URN \"" + et.getDefaultComponentManagerUrn() + "\" is already in the list. Will ignore the one in extra file " + extraFile.getName());
                                }
                            }
                            if (exists) {
                                LOG.debug("Not adding testbed \""+et.getId()+"\" because it already exists.");
                            } else {
                                LOG.debug("Adding non-existing testbed \""+et.getId()+"\" specified in extra file "+extraFile.getName());

                                testbeds.add(giveUniqueId(et, testbeds));
                            }
                        }
                    }
                } catch (AssertionError | Exception e) {
                    LOG.error("Something went wrong while trying to read extra testbeds from " + extraFile, e);
                }
            }
            for (File overwriteFile : getOverwriteTestbedFiles()) {
                try {
                    List<Testbed> overwriteTestbeds = StoredTestbedInfo.load(overwriteFile);
                    if (overwriteTestbeds != null) {
                        for (Testbed ot : overwriteTestbeds) {
                            boolean exists = false;
                            for (ListIterator<Testbed> it = testbeds.listIterator(); it.hasNext(); ) {
                                Testbed existing = it.next();
                                boolean overwrite = false;
                                if (Objects.equals(existing.getId(), ot.getId())) {
                                    LOG.debug("A testbed with ID \"" + ot.getId() + "\" is already in the list. Will overwrite with the one in overwrite file "+overwriteFile.getName());
                                    exists = true;
                                    overwrite = true;
                                }
                                if (existing.getDefaultComponentManagerUrn() != null &&
                                        ot.getDefaultComponentManagerUrn() != null &&
                                        Objects.equals(existing.getDefaultComponentManagerUrn(), ot.getDefaultComponentManagerUrn())) {
                                    exists = true;
                                    overwrite = true;
                                    LOG.debug("A testbed with URN \"" + ot.getDefaultComponentManagerUrn() + "\" is already in the list. Will overwrite with the one in overwrite file "+overwriteFile.getName());
                                }
                                if (overwrite) {
                                    //TODO: assure it has an ID? but for now, the user that overwrites has to make sure unique IDs exist!
                                    it.set(ot);
                                }
                            }
                            if (!exists) {
                                LOG.debug("Adding non-existing testbed \""+ot.getId()+"\" specified in overwrite file "+overwriteFile.getName());

                                testbeds.add(giveUniqueId(ot, testbeds));
                            }
                        }
                    }
                } catch (AssertionError | Exception e) {
                    LOG.error("Something went wrong while trying to read overwrite testbeds from " + overwriteFile, e);
                }
            }
        } else {
            LOG.warn("Will ignore extra and overwrite testbeds, because no testbeds could be found.");
        }

        if (testbeds != null) {
            TestbedInfoSource testbedInfoSource = BasicTestbedInfoSource.createFromTestbeds(testbeds);

            LOG.info("jFed initialised TestbedInfoSource with "+testbedInfoSource.getTestbeds().size()+" testbeds and "+testbedInfoSource.getServers().size()+" servers.");

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

            saveCacheInBackground(testbedInfoSource, cachedTestbedsFile);

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

    private static int extraTestbedUniqueId = 43210;
    private static Testbed giveUniqueId(Testbed orig, List<Testbed> existing) {
        TestbedBuilder res = new TestbedBuilder(orig);
        if (res.getId() == null) {
            res.setId("testbed"+(extraTestbedUniqueId++));
        }
        List<Server> servers = new ArrayList<>(res.getServers());
        res.setServers(null);
        for (Server server : servers) {
            ServerBuilder serverBuilder = new ServerBuilder(server);
            res.addServerBuilder(serverBuilder);
            if (serverBuilder.getId() == null) {
                serverBuilder.setId(extraTestbedUniqueId++);
            }

            serverBuilder.setServices(null);
            if(server.getServices() != null) {
                List<Service> services = new ArrayList<>(server.getServices());
                for (Service service : services) {
                    ServiceBuilder serviceBuilder = new ServiceBuilder(service);
                    serverBuilder.addServiceBuilder(serviceBuilder);
                    if (serviceBuilder.getId() == null) {
                        serviceBuilder.setId(extraTestbedUniqueId++);
                    }
                }
            }
        }
        return res.create();
    }

    private void saveCacheInBackground(final TestbedInfoSource testbedInfoSource, final File targetFile) {
        Runnable r = () -> {
            try {
                save(testbedInfoSource, targetFile);
            } catch (AssertionError | Exception e) {
                LOG.error("Something went wrong while trying to write testbeds to cache " + targetFile, e);
            }
        };
        new Thread(r).start();
    }

    public void save(TestbedInfoSource testbedInfoSource, File targetFile) {
        LOG.info("Saving jFed authority list to file \"" + targetFile + "\"");
        StoredTestbedInfo.saveIgnoringErrors(targetFile, testbedInfoSource);
    }

}
