package com.atlassian.bitbucket.mesh.management;

import com.atlassian.bitbucket.mesh.MeshConstants;
import com.atlassian.bitbucket.mesh.app.BuildInfo;
import com.atlassian.bitbucket.mesh.config.ApplicationConfig;
import com.atlassian.bitbucket.mesh.config.NodeConfig;
import com.atlassian.bitbucket.mesh.git.binary.GitBinary;
import com.atlassian.bitbucket.mesh.process.ProcessParameters;
import com.atlassian.bitbucket.mesh.process.handler.StringStdioHandler;
import com.atlassian.bitbucket.mesh.process.nu.NuProcessCreator;
import com.atlassian.bitbucket.mesh.util.NumberUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.collect.ImmutableMap;
import com.sun.management.UnixOperatingSystemMXBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
import java.time.Duration;
import java.time.ZoneId;
import java.util.Map;
import java.util.OptionalInt;

import static com.atlassian.bitbucket.mesh.management.SupportUtils.APPLICATION_PROPERTIES_JSON;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
 * Provides application and JVM level support information.
 */
public class ApplicationSupportInfoProvider implements SupportInfoProvider {

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

    private final BuildInfo buildInfo;
    private final ApplicationConfig config;
    private final FileSanitizer fileSanitizer;
    private final NuProcessCreator processCreator;

    public ApplicationSupportInfoProvider(BuildInfo buildInfo, ApplicationConfig config, FileSanitizer fileSanitizer,
                                          NuProcessCreator processCreator) {
        this.buildInfo = buildInfo;
        this.config = config;
        this.fileSanitizer = fileSanitizer;
        this.processCreator = processCreator;
    }

    @Override
    public void writeSupportInfo(@Nonnull JsonGenerator json) throws IOException {
        json.writePOJOField("application", getApplicationProperties());
        json.writePOJOField("node", getNodeProperties());
        json.writePOJOField("jvm", getJvmProperties());
        json.writePOJOField("git", getGitProperties());
        json.writePOJOField("java-runtime-environment",
                fileSanitizer.sanitizeMap(ManagementFactory.getRuntimeMXBean().getSystemProperties()));
        json.writePOJOField("environment-variables", fileSanitizer.sanitizeMap(System.getenv()));
        json.writePOJOField("operating-system", getOperatingSystemProperties());
    }

    private Map<String, Object> getJvmProperties() {
        RuntimeMXBean runtimeMx = ManagementFactory.getRuntimeMXBean();

        return ImmutableMap.of(
                "name", runtimeMx.getVmName(),
                "version", runtimeMx.getVmVersion(),
                "vendor", runtimeMx.getVmVendor(),
                "arguments", fileSanitizer.sanitizeList(APPLICATION_PROPERTIES_JSON, runtimeMx.getInputArguments()),
                "time-zone", ZoneId.systemDefault(),
                "uptime", NumberUtils.formatDuration(Duration.ofMillis(runtimeMx.getUptime())));
    }

    private Map<String, Object> getApplicationProperties() {
        return ImmutableMap.of(
                "api-version", MeshConstants.API_CURRENT_VERSION.toString(),
                "build-number", buildInfo.getBuildNumber(),
                "build-timestamp", buildInfo.getBuildTimestamp().toString(),
                "build-version", buildInfo.getBuildVersion(),
                "commit-hash", buildInfo.getCommitHash(),
                "cpus", Runtime.getRuntime().availableProcessors());
    }

    private Map<String, Object> getGitProperties() {
        GitBinary binary = config.getGit().getBinary();
        return ImmutableMap.of(
                "path", binary.getPath(),
                "version", binary.getVersion().toString());
    }

    private OptionalInt getMaxUserProcesses() {
        try {
            String value = processCreator.run(
                    new ProcessParameters.Builder<>(new StringStdioHandler(), summary -> { })
                            .arguments("ulimit", "-u")
                            .build());
            if (isNotBlank(value)) {
                return OptionalInt.of(Integer.parseInt(value));
            }
        } catch (RuntimeException e) {
            log.warn("Could not determine max-user-processes", e);
        }
        return OptionalInt.empty();
    }

    private Map<String, Object> getNodeProperties() {
        NodeConfig node = config.getNode();
        return ImmutableMap.of(
                "id", node.getId(),
                "name", node.getName(),
                "rpc-url", node.getRpcUrl());
    }

    private Map<String, Object> getOperatingSystemProperties() {
        ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();

        OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
        builder.put("os-name", os.getName());
        builder.put("os-architecture", os.getArch());
        builder.put("os-version", os.getVersion());
        builder.put("available-processors", os.getAvailableProcessors());
        builder.put("system-load-average", os.getSystemLoadAverage());

        if (os instanceof com.sun.management.OperatingSystemMXBean) {
            com.sun.management.OperatingSystemMXBean sunOs = (com.sun.management.OperatingSystemMXBean) os;
            builder.put("committed-virtual-memory", NumberUtils.formatSize(sunOs.getCommittedVirtualMemorySize()));
            builder.put("free-swap-space", NumberUtils.formatSize(sunOs.getFreeSwapSpaceSize()));
            builder.put("total-swap-space", NumberUtils.formatSize(sunOs.getTotalSwapSpaceSize()));
            builder.put("free-physical-memory", NumberUtils.formatSize(sunOs.getFreePhysicalMemorySize()));
            builder.put("total-physical-memory", NumberUtils.formatSize(sunOs.getTotalPhysicalMemorySize()));
            builder.put("system-cpu-load", String.format("%.0f%%", 100d * sunOs.getSystemCpuLoad()));
            builder.put("process-cpu-load", String.format("%.0f%%", 100d * sunOs.getProcessCpuLoad()));
        }

        if (os instanceof UnixOperatingSystemMXBean) {
            UnixOperatingSystemMXBean unix = (UnixOperatingSystemMXBean) os;
            builder.put("open-file-descriptor", unix.getOpenFileDescriptorCount());
            builder.put("max-file-descriptor", unix.getMaxFileDescriptorCount());
        }
        getMaxUserProcesses().ifPresent(value -> builder.put("max-user-processes", value));

        return builder.build();
    }
}
