package com.atlassian.bitbucket.mesh.boot;

import com.atlassian.bitbucket.mesh.MeshDatabaseWiring;
import com.atlassian.bitbucket.mesh.Product;
import com.atlassian.bitbucket.mesh.grpc.MeshGrpcWiring;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.sidecar.SidecarChannel;
import com.atlassian.bitbucket.mesh.sidecar.SidecarRunner;
import com.atlassian.bitbucket.mesh.sidecar.WriterSidecarChannel;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.*;
import org.springframework.boot.actuate.autoconfigure.metrics.*;
import org.springframework.boot.actuate.autoconfigure.metrics.export.datadog.DatadogMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.dynatrace.DynatraceMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.influx.InfluxMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.jmx.JmxMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.signalfx.SignalFxMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.StatsdMetricsExportAutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;

import java.io.PrintWriter;
import java.nio.file.Paths;
import java.util.List;

@EnableAspectJAutoProxy(proxyTargetClass = true) //Proxying for gRPC services must be done by class
@EnableScheduling
@ImportAutoConfiguration({
        //Enable JMX auto-configuration, metrics and the Spring Boot admin MBeans
        CompositeMeterRegistryAutoConfiguration.class,
        DatadogMetricsExportAutoConfiguration.class,
        DynatraceMetricsExportAutoConfiguration.class,
        InfluxMetricsExportAutoConfiguration.class,
        JmxAutoConfiguration.class,
        JmxMetricsExportAutoConfiguration.class,
        JvmMetricsAutoConfiguration.class,
        LogbackMetricsAutoConfiguration.class,
        MetricsAutoConfiguration.class,
        SignalFxMetricsExportAutoConfiguration.class,
        StatsdMetricsExportAutoConfiguration.class,
        SpringApplicationAdminJmxAutoConfiguration.class,
        SystemMetricsAutoConfiguration.class,
})
@Import({MeshDatabaseWiring.class, MeshGrpcWiring.class})
@SpringBootApplication
public class MeshApplication {

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

    private static ApplicationContext applicationContext;

    public static void main(String[] args) {
        if (SystemUtils.IS_OS_WINDOWS) {
            log.error("{} does not support Windows.", Product.NAME);
            System.exit(1);
        }

        //Use Spring Boot's ApplicationArguments to parse out option and non-option arguments, to allow them
        //to be specified in any order
        ApplicationArguments arguments = new DefaultApplicationArguments(args);

        //Non-option arguments are treated as commands and used to start/stop the server
        List<String> commands = arguments.getNonOptionArgs();
        if (commands.isEmpty() || "start".equals(commands.get(0))) {
            start(args);
        } else if ("stop".equals(commands.get(0))) {
            stop();
        } else {
            log.warn("Unexpected command(s): {}", commands);
        }
    }

    /**
     * The JVM will not create {@code java.io.tmpdir} if it doesn't exist, so before the application starts,
     * if the directory doesn't exist, try to create it.
     */
    private static void createTmpDir() {
        String tmpDir = System.getProperty("java.io.tmpdir");
        if (tmpDir != null) {
            try {
                MoreFiles.mkdir(Paths.get(tmpDir));
            } catch (IllegalStateException e) {
                log.error("java.io.tmpdir ({}) could not be created", tmpDir, e.getCause());
                System.exit(1);
            }
        }
    }

    private static String getConfigLocations(ApplicationMode mode) {
        //Tell Spring Boot to consider application-private.properties and application-public.properties as
        //fallback configurations
        StringBuilder builder = new StringBuilder()
                .append("classpath:/application-private.properties,")
                .append("classpath:/application-public.properties,");
        //If there are mode-specific properties, include them in the list after the standard fallbacks and
        //before properties from the home directory
        mode.getProperties().ifPresent(properties -> builder.append("classpath:/").append(properties).append(','));

        //Add mesh.properties (and optionally mesh-<profile>.properties files) to give them precedence over the
        //default settings from the classpath
        return builder.append("optional:file:${mesh.home}/,")
                //mesh-node should be last to allow overriding the properties received from the control plane.
                //This has to be marked as optional: because it generally won't exist
                .append("optional:file:${mesh.home}/mesh-node.properties")
                .toString();
    }

    private static ApplicationMode detectMode(String[] args) {
        //Check for a profile specified on the command line
        String propertyValue = null;
        for (String arg : args) {
            if (arg.startsWith("--spring.profiles.active=")) {
                propertyValue = arg.substring(25);
                break;
            }
        }
        if (propertyValue == null) {
            //If the profile wasn't specified on the command line, check the system properties
            propertyValue = System.getProperty("spring.profiles.active");
            if (propertyValue == null) {
                //If no system property was specified, check the environment variables
                propertyValue = System.getenv("SPRING_PROFILES_ACTIVE");
            }
        }

        if (propertyValue != null) {
            //Active profiles accepts comma-delimited profile names, so we need to split first
            String[] profiles = propertyValue.split(",");
            for (String profile : profiles) {
                //Looping over the specified profiles in order, we take the first one that matches a
                //known application mode
                for (ApplicationMode mode : ApplicationMode.values()) {
                    if (profile.equals(mode.getProfile())) {
                        return mode;
                    }
                }
            }
        }

        //If no profile was specified, default to standalone
        return ApplicationMode.STANDALONE;
    }

    /**
     * Applies system properties to control library behavior.
     */
    private static void setSystemProperties(ApplicationMode mode) {
        //Configure Spring Boot
        //Disable automatic auto-configuration. By default, auto-configuration modules are included by Spring
        //Boot if their attended library is present on the classpath. Rather than using classpath detection to
        //control auto-configuration, the application uses @ImportAutoConfiguration to include specific ones
        System.setProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, "false");
        //Tell Spring Boot to load mesh.properties instead of application.properties
        System.setProperty("spring.config.name", "mesh");
        //Tell Spring Boot where to look for configuration files
        System.setProperty("spring.config.location", getConfigLocations(mode));
    }

    private static void start(String[] args) {
        if (applicationContext == null) {
            ApplicationMode mode = detectMode(args);
            setSystemProperties(mode);
            createTmpDir();

            SpringApplication application = new SpringApplication(MeshApplication.class);
            application.addListeners(
                    new LogbackApplicationListener(),
                    new NettyConfigApplicationListener(),
                    new ProfilingApplicationListener());
            application.setAdditionalProfiles(mode.getProfile());
            application.setBannerMode(Banner.Mode.OFF);
            applicationContext = application.run(args);
        } else {
            log.warn("Start command received, but the app is already running");
        }
    }

    private static void stop() {
        if (applicationContext == null) {
            log.warn("Stop command received, but the app is not running");
        } else {
            SpringApplication.exit(applicationContext);
        }
    }

    @Configuration
    @ConditionalOnProperty(value = "sidecar.child.process")
    public static class ChildProcessWiring {

        @Bean
        public CommandLineRunner runner(SidecarChannel channel) {
            return new SidecarRunner(channel);
        }

        @Bean
        public SidecarChannel sidecarChannel() {
            return new WriterSidecarChannel(new PrintWriter(System.out));
        }
    }

    @Configuration
    @ConditionalOnProperty(value = "sidecar.child.process", havingValue = "false", matchIfMissing = true)
    public static class StandaloneProcessWiring {

        //No Bitbucket Server, or started the sidecar from the IDE? No problem!
        @Bean
        public CommandLineRunner runner() {
            return new StandaloneRunner();
        }
    }
}
