package fr.xephi.authme.message.updater;

import ch.jalu.configme.configurationdata.ConfigurationData;
import ch.jalu.configme.configurationdata.PropertyListBuilder;
import ch.jalu.configme.properties.Property;
import ch.jalu.configme.properties.StringProperty;
import ch.jalu.configme.resource.PropertyReader;
import ch.jalu.configme.resource.PropertyResource;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.util.FileUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.Collections.singletonList;

/**
 * Migrates the used messages file to a complete, up-to-date version when necessary.
 */
public class MessageUpdater {

    /**
     * Applies any necessary migrations to the user's messages file and saves it if it has been modified.
     *
     * @param userFile the user's messages file (yml file in the plugin's folder)
     * @param localJarPath path to the messages file in the JAR for the same language (may not exist)
     * @param defaultJarPath path to the messages file in the JAR for the default language
     * @return true if the file has been migrated and saved, false if it is up-to-date
     */
    public boolean migrateAndSave(File userFile, String localJarPath, String defaultJarPath) {
        JarMessageSource jarMessageSource = new JarMessageSource(localJarPath, defaultJarPath);
        return migrateAndSave(userFile, jarMessageSource);
    }

    /**
     * Performs the migration.
     *
     * @param userFile the file to verify and migrate
     * @param jarMessageSource jar message source to get texts from if missing
     * @return true if the file has been migrated and saved, false if it is up-to-date
     */
    private boolean migrateAndSave(File userFile, JarMessageSource jarMessageSource) {
        // YamlConfiguration escapes all special characters when saving, making the file hard to use, so use ConfigMe
        MessageKeyConfigurationData configurationData = createConfigurationData();
        PropertyResource userResource = new MigraterYamlFileResource(userFile);

        PropertyReader reader = userResource.createReader();
        configurationData.initializeValues(reader);

        // Step 1: Migrate any old keys in the file to the new paths
        boolean movedOldKeys = migrateOldKeys(reader, configurationData);
        // Step 2: Perform newer migrations
        boolean movedNewerKeys = migrateKeys(reader, configurationData);
        // Step 3: Take any missing messages from the message files shipped in the AuthMe JAR
        boolean addedMissingKeys = addMissingKeys(jarMessageSource, configurationData);

        if (movedOldKeys || movedNewerKeys || addedMissingKeys) {
            backupMessagesFile(userFile);

            userResource.exportProperties(configurationData);
            ConsoleLogger.debug("Successfully saved {0}", userFile);
            return true;
        }
        return false;
    }

    private boolean migrateKeys(PropertyReader propertyReader, MessageKeyConfigurationData configurationData) {
        return moveIfApplicable(propertyReader, configurationData,
            "misc.two_factor_create", MessageKey.TWO_FACTOR_CREATE);
    }

    private static boolean moveIfApplicable(PropertyReader reader, MessageKeyConfigurationData configurationData,
                                            String oldPath, MessageKey messageKey) {
        if (configurationData.getMessage(messageKey) == null && reader.getString(oldPath) != null) {
            configurationData.setMessage(messageKey, reader.getString(oldPath));
            return true;
        }
        return false;
    }

    private boolean migrateOldKeys(PropertyReader propertyReader, MessageKeyConfigurationData configurationData) {
        boolean hasChange = OldMessageKeysMigrater.migrateOldPaths(propertyReader, configurationData);
        if (hasChange) {
            ConsoleLogger.info("Old keys have been moved to the new ones in your messages_xx.yml file");
        }
        return hasChange;
    }

    private boolean addMissingKeys(JarMessageSource jarMessageSource, MessageKeyConfigurationData configurationData) {
        List<String> addedKeys = new ArrayList<>();
        for (Property<String> property : configurationData.getAllMessageProperties()) {
            final String key = property.getPath();
            if (configurationData.getValue(property) == null) {
                configurationData.setValue(property, jarMessageSource.getMessageFromJar(property));
                addedKeys.add(key);
            }
        }
        if (!addedKeys.isEmpty()) {
            ConsoleLogger.info(
                "Added " + addedKeys.size() + " missing keys to your messages_xx.yml file: " + addedKeys);
            return true;
        }
        return false;
    }

    private static void backupMessagesFile(File messagesFile) {
        String backupName = FileUtils.createBackupFilePath(messagesFile);
        File backupFile = new File(backupName);
        try {
            Files.copy(messagesFile, backupFile);
        } catch (IOException e) {
            throw new IllegalStateException("Could not back up '" + messagesFile + "' to '" + backupFile + "'", e);
        }
    }

    /**
     * Constructs the {@link ConfigurationData} for exporting a messages file in its entirety.
     *
     * @return the configuration data to export with
     */
    public static MessageKeyConfigurationData createConfigurationData() {
        Map<String, String> comments = ImmutableMap.<String, String>builder()
            .put("registration", "Registration")
            .put("password", "Password errors on registration")
            .put("login", "Login")
            .put("error", "Errors")
            .put("antibot", "AntiBot")
            .put("unregister", "Unregister")
            .put("misc", "Other messages")
            .put("session", "Session messages")
            .put("on_join_validation", "Error messages when joining")
            .put("email", "Email")
            .put("recovery", "Password recovery by email")
            .put("captcha", "Captcha")
            .put("verification", "Verification code")
            .put("time", "Time units")
            .put("two_factor", "Two-factor authentication")
            .build();

        Set<String> addedKeys = new HashSet<>();
        MessageKeyPropertyListBuilder builder = new MessageKeyPropertyListBuilder();
        // Add one key per section based on the comments map above so that the order is clear
        for (String path : comments.keySet()) {
            MessageKey key = Arrays.stream(MessageKey.values()).filter(p -> p.getKey().startsWith(path + "."))
                .findFirst().orElseThrow(() -> new IllegalStateException(path));
            builder.addMessageKey(key);
            addedKeys.add(key.getKey());
        }
        // Add all remaining keys to the property list builder
        Arrays.stream(MessageKey.values())
            .filter(key -> !addedKeys.contains(key.getKey()))
            .forEach(builder::addMessageKey);

        // Create ConfigurationData instance
        Map<String, List<String>> commentsMap = comments.entrySet().stream()
            .collect(Collectors.toMap(e -> e.getKey(), e -> singletonList(e.getValue())));
        return new MessageKeyConfigurationData(builder, commentsMap);
    }

    static final class MessageKeyProperty extends StringProperty {

        MessageKeyProperty(MessageKey messageKey) {
            super(messageKey.getKey(), "");
        }

        @Override
        protected String getFromReader(PropertyReader reader) {
            return reader.getString(getPath());
        }
    }

    static final class MessageKeyPropertyListBuilder {

        private PropertyListBuilder propertyListBuilder = new PropertyListBuilder();

        void addMessageKey(MessageKey key) {
            propertyListBuilder.add(new MessageKeyProperty(key));
        }

        @SuppressWarnings("unchecked")
        List<MessageKeyProperty> getAllProperties() {
            return (List) propertyListBuilder.create();
        }
    }
}
