package com.atlassian.bitbucket.mesh.boot;

import com.atlassian.bitbucket.mesh.boot.config.DirectMemoryConfig;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;

import javax.annotation.Nonnull;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.Long.parseLong;
import static java.lang.Math.min;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.util.Objects.requireNonNull;
import static org.springframework.util.unit.DataSize.ofBytes;
import static org.springframework.util.unit.DataSize.parse;

public class NettyConfigApplicationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    private static final Pattern MAX_DIRECT_MEMORY_SIZE_ARG_PATTERN =
            Pattern.compile("\\s*-XX:MaxDirectMemorySize\\s*=\\s*([0-9]+)\\s*([kKmMgG]?)\\s*$");
    private static final String PROP_GRPC_DIRECT_MEMORY_PREFIX = "grpc.direct-memory";
    private static final String PROP_NETTY_MAX_DIRECT_MEMORY = "io.netty.maxDirectMemory";

    private static final Logger log = LoggerFactory.getLogger(NettyConfigApplicationListener.class);

    @Override
    public void onApplicationEvent(@Nonnull ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment env = requireNonNull(event, "event").getEnvironment();

        long jvmMaxDirectMemory = getMaxDirectMemory();
        DirectMemoryConfig directMemoryConfig = Binder.get(env).bindOrCreate(PROP_GRPC_DIRECT_MEMORY_PREFIX, DirectMemoryConfig.class);

        long effectiveNettyMaxDirectMemory = min(
                directMemoryConfig.getMax().toBytes() < 0 ? jvmMaxDirectMemory : directMemoryConfig.getMax().toBytes(),
                jvmMaxDirectMemory - directMemoryConfig.getReserved().toBytes());

        // Make sure we always have a sane value for Netty's direct memory limit
        if (effectiveNettyMaxDirectMemory < 0L || effectiveNettyMaxDirectMemory > jvmMaxDirectMemory) {
            effectiveNettyMaxDirectMemory = jvmMaxDirectMemory - parse("50MB").toBytes();
        }

        Long configuredNettyMaxDirectMemory = env.getProperty(PROP_NETTY_MAX_DIRECT_MEMORY, Long.class);
        if (configuredNettyMaxDirectMemory != null) {
            if (configuredNettyMaxDirectMemory <= effectiveNettyMaxDirectMemory) {
                // If the configured direct memory limit for Netty is less than the limit we calculate, we use the same value
                effectiveNettyMaxDirectMemory = configuredNettyMaxDirectMemory;
            } else {
                log.info("Configured direct memory limit for Netty is too high. Setting it to {} MB instead of {} MB.",
                        ofBytes(effectiveNettyMaxDirectMemory).toMegabytes(),
                        ofBytes(configuredNettyMaxDirectMemory).toMegabytes());
            }
        } else {
            log.debug("Setting direct memory limit for Netty to {}MB and reserving {}MB for other processes",
                    ofBytes(effectiveNettyMaxDirectMemory).toMegabytes(), directMemoryConfig.getReserved().toMegabytes());
        }

        // Adding this to a PropertySource and attaching it to the environment doesn't work because Netty checks
        // for properties using System.getProperty
        System.setProperty(PROP_NETTY_MAX_DIRECT_MEMORY, valueOf(effectiveNettyMaxDirectMemory));
    }

    @VisibleForTesting
    static long getMaxDirectMemory() {
        long maxDirectMemory = 0L;

        // Check if -XX:MaxDirectMemorySize is set
        List<String> vmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
        for (String arg : vmArgs) {
            Matcher m = MAX_DIRECT_MEMORY_SIZE_ARG_PATTERN.matcher(arg);
            if (!m.matches()) {
                continue;
            }
            String maxDirectMemoryArgument = format("%d%sB", parseLong(m.group(1)), m.group(2).toUpperCase(Locale.ROOT));
            maxDirectMemory = parse(maxDirectMemoryArgument).toBytes();
        }

        return maxDirectMemory > 0L ? maxDirectMemory : Runtime.getRuntime().maxMemory();
    }
}
