package supervisor

import (
	"net/http"
	"os"
	"os/exec"
	"strconv"
	"sync/atomic"
	"time"

	"github.com/pingcap/log"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/spf13/pflag"
	"go.uber.org/zap"
)

var (
	agentProcessIsRunning atomic.Value
	processIsRunningGauge = promauto.NewGauge(prometheus.GaugeOpts{
		Namespace: "dbaas",
		Subsystem: "topsql",
		Name:      "agent_process_healthy",
		Help:      "If the Top SQL agent is running, this gauge has value 1, else -1",
	})
)

func Run(metricsAddr string, flags *pflag.FlagSet) {
	log.Info("[supervisor] start supervisor")

	go reconcileAgentProcess(flags)
	go runAgentProcessStatusWatcher()
	runPrometheusMetricsHandler(metricsAddr)
}

// reconcileAgentProcess uses a simple control loop to reconcile the agent process.
// It will restart the agent process if the process is not running.
func reconcileAgentProcess(flags *pflag.FlagSet) {
	interval, _ := flags.GetUint("reconcile-interval")
	args := assembleArgs(flags)

	for {
		if agentProcessIsRunning.Load() == nil || !agentProcessIsRunning.Load().(bool) {
			runAgentProcess(args)
		}

		time.Sleep(time.Duration(interval) * time.Second)
	}
}

// assembleArgs assembles the command line arguments of agent so that they can be reused.
func assembleArgs(flags *pflag.FlagSet) []string {
	debug, _ := flags.GetBool("debug")
	store, _ := flags.GetString("store")
	agentAddr, _ := flags.GetString("agent-address")
	metricsAddr, _ := flags.GetString("metrics-address")
	instanceID, _ := flags.GetString("instance-id")
	instanceName, _ := flags.GetString("instance-name")
	instanceComp, _ := flags.GetString("instance-component")
	clusterID, _ := flags.GetString("cluster-id")
	tlsCAPath, _ := flags.GetString("tls-ca")
	tlsCertPath, _ := flags.GetString("tls-cert")
	tlsKeyPath, _ := flags.GetString("tls-key")

	log.Info("[supervisor] os.Args", zap.Strings("value", os.Args), zap.Any("debug", strconv.FormatBool(debug)))
	args := []string{"agent"}
	if debug {
		args = append(args, "--debug")
	}
	args = append(args,
		"--store", store,
		"--agent-address", agentAddr,
		"--metrics-address", metricsAddr,
		"--instance-id", instanceID,
		"--instance-name", instanceName,
		"--instance-component", instanceComp,
		"--cluster-id", clusterID,
	)
	if tlsCAPath != "" {
		args = append(args, "--tls-ca", tlsCAPath)
	}
	if tlsCertPath != "" {
		args = append(args, "--tls-cert", tlsCertPath)
	}
	if tlsKeyPath != "" {
		args = append(args, "--tls-key", tlsKeyPath)
	}
	return args
}

// runAgentProcess runs the agent process by `exec.Command`.
func runAgentProcess(args []string) {
	agentProcessIsRunning.Store(true)

	// G204: Subprocess launched with function call as argument or cmd arguments (gosec)
	cmd := exec.Command(os.Args[0], args...) //nolint:gosec // Reason: by design
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	log.Info("[supervisor] start topsql agent process")

	if err := cmd.Start(); err != nil {
		log.Error("[supervisor] start agent process failed", zap.Error(err))
	} else if err := cmd.Wait(); err != nil {
		log.Error("[supervisor] agent process exited", zap.Error(err))
	}

	agentProcessIsRunning.Store(false)
}

func runAgentProcessStatusWatcher() {
	for {
		if agentProcessIsRunning.Load() != nil && agentProcessIsRunning.Load().(bool) {
			processIsRunningGauge.Set(1)
		} else {
			processIsRunningGauge.Set(-1)
		}
		time.Sleep(5 * time.Second)
	}
}

func runPrometheusMetricsHandler(metricsAddr string) {
	log.Info("[supervisor] start prometheus metrics server")
	http.Handle("/metrics", promhttp.Handler())
	if err := http.ListenAndServe(metricsAddr, nil); err != nil {
		log.Fatal("[supervisor] failed to start metrics server", zap.Error(err))
	}
}
