package cz.moneta.test.harness.config;

import cz.moneta.test.harness.constants.HarnessConfigConstants;
import cz.moneta.test.harness.exception.HarnessConfigurationException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class ConfigProvider {

    private static final Logger logger = LogManager.getLogger(ConfigProvider.class);

    private ConfigProvider() {}

    public static Map<String, String> readConfig() {
        String fileName = Stream.<Supplier<String>>of(
                () -> System.getProperty("config"),
                () -> System.getenv("HARNESS_CONFIG"))
                .map(Supplier::get)
                .filter(Objects::nonNull)
                .findFirst()
                .orElseThrow(() -> new HarnessConfigurationException("You need to provide a configuration file"));

        Map<String, String> resultConfig = loadConfig(fileName, new ArrayList<>());
        logger.debug("Effective configuration:");
        resultConfig.entrySet()
                .stream()
                .filter(configEntry -> !configEntry.getKey().equals(HarnessConfigConstants.VAULT_PASSWORD_CONFIG)) //DO NOT LOG VAULT PWD VALUE!
                .collect(Collectors.toMap(Map.Entry::getKey, configEntry -> configEntry.getValue().trim()))
                .forEach((key, value) -> logger.debug("{}={}", key, value));
        return resultConfig;
    }

    private static Map<String, String> loadConfig(String fileName, List<String> configStack) {
        Map<String, String> properties = readProperties(fileName);

        return Optional.ofNullable(properties.get("environment.from"))
                .map(fn -> {
                    if (configStack.contains(fn)) {
                        throw new IllegalStateException("Config chain contains circular dependencies");
                    }
                    return fn;
                })
                .map(fn -> {
                    configStack.add(fn);
                    return loadConfig(fn, configStack);
                })
                .map(p -> {
                    p.putAll(properties);
                    return p;
                })
                .orElse(properties);
    }

    private static Map<String, String> readProperties(String fileName) {
        Reader reader;
        try {
            logger.debug(() -> String.format("Trying to load config file: %s", fileName));
            reader = new FileReader(fileName);
        } catch (FileNotFoundException e) {
            logger.debug(() ->
                    String.format("Could not load file: %s, trying to load it as classpath resource", fileName));
            reader = getReaderForClasspathResource(fileName);
        }

        Properties properties = new Properties();
        try {
            properties.load(reader);
        } catch (IOException e) {
            throw new IllegalStateException("Cannot read config file", e);
        }

        return properties.stringPropertyNames().stream()
                .collect(Collectors.toMap(k -> k, k -> resolveSystemProperty(properties.getProperty(k))));
    }

    private static String resolveSystemProperty(String value) {
        return Stream.<Supplier<Optional<String>>>of(
                () -> Optional.ofNullable(value).filter(v -> !value.startsWith("$")),
                () -> Optional.ofNullable(value)
                        .map(v -> System.getProperty(v.substring(1)))
                        .map(v -> {
                            logger.debug("Successfully resolved system property: {}", () -> value);
                            return v;
                        }))
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("Cannot find system property for: " + value));
    }

    private static Reader getReaderForClasspathResource(String resourceName) {
        String resourcePath = "envs/" + resourceName; //TODO hidden magic constant - fix this concept
        InputStream resourceAsStream = ConfigProvider.class.getClassLoader().getResourceAsStream(resourcePath);
        if (resourceAsStream != null) {
            return new InputStreamReader(resourceAsStream);
        } else {
            throw new IllegalStateException(String.format("Cannot find config file: %s", resourceName));
        }
    }
}
