package com.atlassian.bitbucket.mesh.config;

import com.atlassian.bitbucket.mesh.logback.LogbackUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.core.env.Environment;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

import static com.google.common.collect.Sets.difference;
import static java.util.Comparator.comparing;
import static java.util.Map.Entry.comparingByKey;

public class DefaultLoggingConfig implements LoggingConfig {

    private static final String ROOT = "ROOT";
    //This should be kept in sync with logback-spring.xml
    private static final Map<String, String> DEFAULT_LEVELS = ImmutableMap.of(
            ROOT, "WARN",
            "com.atlassian.bitbucket", "INFO",
            "org.springframework.boot.SpringApplication", "INFO");

    private final Set<String> changedLoggers;
    private final Environment environment;
    private final BiConsumer<String, String> logLevelMutator;

    private volatile Settings current;

    public DefaultLoggingConfig(Environment environment) {
        this(environment, LogbackUtils::setLevel);
    }

    @VisibleForTesting
    DefaultLoggingConfig(Environment environment, BiConsumer<String, String> logLevelMutator) {
        this.environment = environment;

        //Don't apply the settings initially because they're already applied on startup
        current = new Settings(environment);
        changedLoggers = new HashSet<>(current.levelsByLogger.keySet());
        this.logLevelMutator = logLevelMutator;
    }

    @Nonnull
    @Override
    public Map<String, String> getLevelsByLogger() {
        return current.levelsByLogger;
    }

    void reload() {
        current = new Settings(environment);
        current.apply();
    }

    private static String findDefaultLogLevel(String logger) {
        return DEFAULT_LEVELS.entrySet().stream()
                .filter(entry -> logger.startsWith(entry.getKey()))
                .max(comparingByKey(comparing(String::length)))
                .map(Map.Entry::getValue)
                .orElse(DEFAULT_LEVELS.get(ROOT));
    }

    private class Settings {

        private final Map<String, String> levelsByLogger;

        Settings(Environment environment) {
            levelsByLogger = ImmutableMap.copyOf(Binder.get(environment)
                    .bind("logging.logger", Bindable.mapOf(String.class, String.class))
                    .orElseGet(Collections::emptyMap));
        }

        void apply() {
            synchronized (DefaultLoggingConfig.this) {
                levelsByLogger.forEach((loggerName, levelName) -> {
                    changedLoggers.add(loggerName);
                    logLevelMutator.accept(loggerName, levelName);
                });
                difference(changedLoggers, levelsByLogger.keySet()).forEach(logger ->
                        logLevelMutator.accept(logger, findDefaultLogLevel(logger)));
                changedLoggers.retainAll(levelsByLogger.keySet());
            }
        }
    }
}
