/*
 * DiscordSRV - A Minecraft to Discord and back link plugin
 * Copyright (C) 2016-2020 Austin "Scarsz" Shapiro
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package github.scarsz.discordsrv.util;

import com.github.zafarkhaja.semver.Version;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import github.scarsz.configuralize.ParseException;
import github.scarsz.configuralize.Provider;
import github.scarsz.configuralize.Source;
import github.scarsz.discordsrv.DiscordSRV;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

public class ConfigUtil {

    public static void migrate() {
        String configVersionRaw = DiscordSRV.config().getString("ConfigVersion");
        String pluginVersionRaw = DiscordSRV.getPlugin().getDescription().getVersion();
        if (configVersionRaw.equals(pluginVersionRaw)) return;

        Version configVersion = configVersionRaw.split("\\.").length == 3
                ? Version.valueOf(configVersionRaw.replace("-SNAPSHOT", ""))
                : Version.valueOf("1." + configVersionRaw.replace("-SNAPSHOT", ""));
        Version pluginVersion = Version.valueOf(pluginVersionRaw.replace("-SNAPSHOT", ""));

        if (configVersion.equals(pluginVersion)) return; // no migration necessary
        if (configVersion.greaterThan(pluginVersion)) {
            DiscordSRV.warning("You're attempting to use a higher config version than the plugin. Things probably won't work correctly.");
            return;
        }

        String oldVersionName = configVersion.toString();
        DiscordSRV.info("Your DiscordSRV config file was outdated; attempting migration...");

        try {
            Provider configProvider = DiscordSRV.config().getProvider("config");
            Provider messageProvider = DiscordSRV.config().getProvider("messages");
            Provider voiceProvider = DiscordSRV.config().getProvider("voice");
            Provider linkingProvider = DiscordSRV.config().getProvider("linking");
            Provider synchronizationProvider = DiscordSRV.config().getProvider("synchronization");

            if (configVersion.greaterThanOrEqualTo(Version.forIntegers(1, 13, 0))) {
                migrate("messages.yml-build." + oldVersionName + ".old", DiscordSRV.getPlugin().getMessagesFile(), messageProvider);
                migrate("config.yml-build." + oldVersionName + ".old", DiscordSRV.getPlugin().getConfigFile(), configProvider, false);
                migrate("voice.yml-build." + oldVersionName + ".old", DiscordSRV.getPlugin().getVoiceFile(), voiceProvider);
                migrate("linking.yml-build." + oldVersionName + ".old", DiscordSRV.getPlugin().getLinkingFile(), linkingProvider, true);
                migrate("synchronization.yml-build." + oldVersionName + ".old", DiscordSRV.getPlugin().getSynchronizationFile(), synchronizationProvider);
            } else {
                // legacy migration <1.13.0
                // messages
                File messagesFrom = new File(DiscordSRV.getPlugin().getDataFolder(), "config.yml");
                File messagesTo = DiscordSRV.getPlugin().getMessagesFile();
                messageProvider.saveDefaults();
                copyYmlValues(messagesFrom, messagesTo, false);
                messageProvider.load();

                // config
                File configFrom = new File(DiscordSRV.getPlugin().getDataFolder(), "config.yml-build." + oldVersionName + ".old");
                File configTo = DiscordSRV.getPlugin().getConfigFile();
                FileUtils.moveFile(configTo, configFrom);
                configProvider.saveDefaults();
                copyYmlValues(configFrom, configTo, false);
                configProvider.load();

                // channels
                File channelsFile = new File(DiscordSRV.getPlugin().getDataFolder(), "channels.json");
                if (channelsFile.exists()) {
                    List<Map<String, String>> channels = new ArrayList<>();
                    JsonArray jsonElements = DiscordSRV.getPlugin().getGson().fromJson(FileUtils.readFileToString(channelsFile, StandardCharsets.UTF_8), JsonArray.class);
                    for (JsonElement jsonElement : jsonElements) {
                        channels.add(new HashMap<String, String>() {{
                            put(jsonElement.getAsJsonObject().get("channelname").getAsString(), jsonElement.getAsJsonObject().get("channelid").getAsString());
                        }});
                    }
                    String channelsString = "{" + channels.stream()
                            .map(stringStringMap -> "\"" + stringStringMap.keySet().iterator().next() + "\": \"" + stringStringMap.values().iterator().next() + "\"")
                            .collect(Collectors.joining(", ")) + "}";
                    FileUtils.writeStringToFile(channelsFile, "Channels: " + channelsString, StandardCharsets.UTF_8);
                    copyYmlValues(channelsFile, configTo, false);
                    channelsFile.delete();
                }

                // colors
                File colorsFile = new File(DiscordSRV.getPlugin().getDataFolder(), "colors.json");
                FileUtils.moveFile(colorsFile, new File(colorsFile.getParent(), "colors.json.old"));
            }
            DiscordSRV.info("Successfully migrated configuration files to version " + configVersionRaw);
        } catch (Exception e) {
            DiscordSRV.error("Failed migrating configs: " + e.getMessage());
            DiscordSRV.debug(ExceptionUtils.getStackTrace(e));
        }
    }

    private static void migrate(String fromFileName, File to, Provider provider) throws IOException, ParseException {
        migrate(fromFileName, to, provider, false);
    }

    private static void migrate(String fromFileName, File to, Provider provider, boolean allowSpacedOptions) throws IOException, ParseException {
        File from = new File(DiscordSRV.getPlugin().getDataFolder(), fromFileName);
        FileUtils.moveFile(to, from);
        provider.saveDefaults();
        copyYmlValues(from, to, allowSpacedOptions);
        provider.load();
    }

    private static void copyYmlValues(File from, File to, boolean allowSpacedOptions) {
        try {
            List<String> oldConfigLines = Arrays.stream(FileUtils.readFileToString(from, StandardCharsets.UTF_8).split(System.lineSeparator() + "|\n")).collect(Collectors.toList());
            List<String> newConfigLines = Arrays.stream(FileUtils.readFileToString(to, StandardCharsets.UTF_8).split(System.lineSeparator() + "|\n")).collect(Collectors.toList());

            Map<String, String> oldConfigMap = new HashMap<>();
            for (String line : oldConfigLines) {
                if (line.startsWith("#") || line.startsWith("-") || line.isEmpty() || StringUtils.isBlank(line.substring(0, 1))) continue;
                String[] lineSplit = line.split(":", 2);
                if (lineSplit.length != 2) continue;
                String key = lineSplit[0];
                String value = lineSplit[1].trim();
                oldConfigMap.put(key, value);
            }

            Map<String, String> newConfigMap = new HashMap<>();
            for (String line : newConfigLines) {
                if (line.startsWith("#") || line.startsWith("-") || line.isEmpty() || (!allowSpacedOptions && StringUtils.isBlank(line.substring(0, 1)))) continue;
                String[] lineSplit = line.split(":", 2);
                if (lineSplit.length != 2) continue;
                String key = lineSplit[0];
                String value = lineSplit[1].trim();
                newConfigMap.put(key, value);
            }

            for (String key : oldConfigMap.keySet()) {
                if (newConfigMap.containsKey(key) && !key.startsWith("ConfigVersion")) {
                    DiscordSRV.debug("Migrating config option " + key + " with value " + (DebugUtil.SENSITIVE_OPTIONS.stream().anyMatch(key::equalsIgnoreCase) ? "OMITTED" : oldConfigMap.get(key)) + " to new config");
                    newConfigMap.put(key, oldConfigMap.get(key));
                }
            }

            for (String line : newConfigLines) {
                if (line.startsWith("#") || line.isEmpty()) continue;
                if (line.startsWith("ConfigVersion")) {
                    newConfigLines.set(newConfigLines.indexOf(line), line);
                    continue;
                }
                String key = line.split(":")[0];
                if (oldConfigMap.containsKey(key)) {
                    newConfigLines.set(newConfigLines.indexOf(line), key + ": " + newConfigMap.get(key));
                }
            }

            FileUtils.writeStringToFile(to, String.join(System.lineSeparator(), newConfigLines), StandardCharsets.UTF_8);
        } catch (Exception e) {
            DiscordSRV.warning("Failed to migrate config: " + e.getMessage());
            e.printStackTrace();
        }
    }

    public static void logMissingOptions() {
        for (Map.Entry<Source, Provider> entry : DiscordSRV.config().getSources().entrySet()) {
            Set<String> keys = getAllKeys(entry.getValue().getDefaults().asMap());
            keys.removeAll(getAllKeys(entry.getValue().getValues().asMap()));

            for (String missing : keys) {
                DiscordSRV.warning("Config key " + missing + " is missing from the " + entry.getKey().getResourceName() + ".yml. Using the default value of " + entry.getValue().getDefaults().dget(missing).asString());
            }
        }
    }

    public static Set<String> getAllKeys(Map<String, Object> map) {
        return getAllKeys(map, null);
    }
    public static Set<String> getAllKeys(Map<String, Object> map, String prefix) {
        Set<String> keys = new HashSet<>();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = (prefix != null ? prefix + "." : "") + entry.getKey();
            keys.add(key);

            if (entry.getValue() instanceof Map) keys.addAll(getAllKeys((Map) entry.getValue(), key));
        }
        return keys;
    }

}