// Copyright (c) 2023 Volvo Car Corporation
// SPDX-License-Identifier: Apache-2.0

// Code generated by lingon. EDIT AS MUCH AS YOU LIKE.

package nats

import (
	"bytes"
	"fmt"
	"strings"

	ku "github.com/golingon/lingon/pkg/kubeutil"
	"github.com/golingon/lingoneks/meta"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
	ResourceCPU     = 2
	ResourceMemNum  = 4
	ResourceMemUnit = "Gi"
	ResourceJSMem   = "2G"
	ResourceStorage = "10Gi"
)

var (
	N          = Core()
	configFile = "nats.conf"
)

func Core() Meta {
	n := "nats"
	ns := n
	promExporterV := "0.12.0"
	// reloaderV := "0.11.0"
	reloaderV := "latest"
	ver := "2.9.19"

	m := meta.Metadata{
		Name:      n,
		Namespace: ns,
		Instance:  "nats-server",
		Component: "nats-server",
		PartOf:    n,
		Version:   ver,
		ManagedBy: "lingon",

		Img: meta.ContainerImg{
			Registry: "docker.io",
			Image:    n,
			Tag:      ver + "-alpine",
		},
	}

	configBasePath := "/etc/nats-config"
	configPath := configBasePath + "/" + configFile
	natsPIDBasePath := "/var/run/nats"
	np := func(p int32, name string, ap *string) meta.NetPort {
		return meta.NetPort{
			Container: corev1.ContainerPort{Name: name, ContainerPort: p},
			Service: corev1.ServicePort{
				Name: name, Port: p, AppProtocol: ap,
			},
		}
	}
	HTTP := P("http")
	TCP := P("tcp")

	res := Meta{
		Metadata: m,

		ConfigFile: configFile,
		ConfigPath: configPath,

		replicas: 3,

		// PORTS
		Client:  np(4222, "client", TCP),
		Cluster: np(6222, "cluster", TCP),
		Monitor: np(8222, "monitor", HTTP),
		Metrics: np(7777, "metrics", HTTP),
		Leaf:    np(7422, "leafnodes", TCP),
		Gw:      np(7522, "gateways", TCP),

		// STORAGE
		PvcName:      n + "js-pvc",
		storageDir:   "/data",
		StorageClass: "gp2",

		pidPath: natsPIDBasePath + "/nats.pid",
		pidVM:   corev1.VolumeMount{MountPath: natsPIDBasePath, Name: "pid"},

		// SIDE CAR
		ConfigReloader: meta.ContainerImg{
			Image: "natsio/nats-server-config-reloader",
			Tag:   reloaderV,
		},
		PromExporter: meta.ContainerImg{
			Image: "natsio/prometheus-nats-exporter",
			Tag:   promExporterV,
		},
	}
	// ConfigMap and Mount
	res.Config = ku.ConfigAndMount{
		Data: map[string]string{res.ConfigFile: Config(res)},
		ObjectMeta: metav1.ObjectMeta{
			Labels:    m.Labels(),
			Name:      "nats-config",
			Namespace: m.Namespace,
		},
		VolumeMount: corev1.VolumeMount{
			Name:      "config-volume",
			MountPath: configBasePath,
		},
	}

	return res
}

type Meta struct {
	meta.Metadata

	pidVM corev1.VolumeMount

	PromExporter   meta.ContainerImg
	ConfigReloader meta.ContainerImg

	pidPath      string
	ConfigFile   string
	ConfigPath   string
	PvcName      string
	StorageClass string
	storageDir   string

	Config ku.ConfigAndMount

	Client  meta.NetPort
	Gw      meta.NetPort
	Leaf    meta.NetPort
	Metrics meta.NetPort
	Monitor meta.NetPort
	Cluster meta.NetPort

	replicas int
}

var (
	d = func(i int32) string { return fmt.Sprintf("%d", i) }

	ResourceMemory = d(ResourceMemNum) + ResourceMemUnit
)

func Config(n Meta) string {
	return `
# NATS Clients Port
port: ` + d(n.Client.Container.ContainerPort) + `
# PID file shared with configuration reloader.
pid_file: "` + n.pidPath + `"
###############
#             #
# Monitoring  #
#             #
###############
` + ConfigMonitoring(
		n.Monitor.Container.ContainerPort,
		[]string{"cloud:aws", "region:eu-north1", ResourceMemory},
	) + `
###################################
#                                 #
# NATS JetStream                  #
#                                 #
###################################
` + ConfigJetStream(
		ResourceJSMem,
		n.storageDir,
		ResourceStorage,
		"srv",
	) + `

###################################
#                                 #
# NATS Full Mesh Clustering Setup #
#                                 #
###################################
` + ConfigCluster(
		"natscluster",
		n.Cluster.Container.ContainerPort,
		n.srvURLs(n.replicas),
		120,
	) + `

lame_duck_grace_period: 10s
lame_duck_duration: 30s
`
}

func (n Meta) srvURLs(r int) string {
	var res bytes.Buffer
	res.WriteString("\n")
	for i := 0; i < r; i++ {
		u := fmt.Sprintf(
			"    nats://%s-%d.%s.%s.svc.cluster.local:%d",
			n.Name, i, n.Name, n.Namespace,
			n.Cluster.Service.Port,
		)
		res.WriteString(u)
		res.WriteString("\n")
	}
	return res.String()
}

func ConfigMonitoring(port int32, tags []string) string {
	return `
http: ` + d(port) + `
server_name:$POD_NAME
server_tags: [
  "` + strings.Join(tags, ",") + `"
]
`
}

// ConfigJetStream returns a string containing the jetstream config.
// For more info: https://docs.nats.io/nats-concepts/jetstream/streams
func ConfigJetStream(maxMem, storeDir, maxFile, uniqTag string) string {
	return `
jetstream {
  store_dir: "` + storeDir + `"
  max_mem_store:` + maxMem + `
  max_file_store:` + maxFile + `
}
`
}

func ConfigCluster(
	name string,
	port int32,
	routes string,
	retries int32,
) string {
	return `
cluster {
  name: ` + name + `
  port: ` + d(port) + `
  routes = [
` + routes + `
  ]
  cluster_advertise: $CLUSTER_ADVERTISE
  connect_retries: ` + d(retries) + `
}
`
}

var SVC = &corev1.Service{
	TypeMeta:   ku.TypeServiceV1,
	ObjectMeta: N.ObjectMeta(),
	Spec: corev1.ServiceSpec{
		// Headless Services - no load balancing
		// https://kubernetes.io/docs/concepts/services-networking/service/#headless-services
		ClusterIP: corev1.ClusterIPNone,
		Ports: []corev1.ServicePort{
			N.Client.Service,
			N.Cluster.Service,
			N.Metrics.Service,
			N.Monitor.Service,
			N.Leaf.Service,
		},
		PublishNotReadyAddresses: true,
		Selector:                 N.MatchLabels(),
	},
}

var PAA = &corev1.Affinity{
	PodAntiAffinity: &corev1.PodAntiAffinity{
		RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{
			{
				LabelSelector: &metav1.LabelSelector{
					MatchExpressions: []metav1.LabelSelectorRequirement{
						{
							Key:      ku.AppLabelName,
							Operator: metav1.LabelSelectorOpIn,
							Values:   []string{N.Name},
						},
					},
				},
				TopologyKey: corev1.LabelHostname,
			},
		},
	},
}

var STS = &appsv1.StatefulSet{
	TypeMeta: ku.TypeStatefulSetV1,
	ObjectMeta: metav1.ObjectMeta{
		Labels:    ku.SetLabelDefaultContainer(N.Labels(), N.Name),
		Name:      N.Name,
		Namespace: N.Namespace,
	},
	Spec: appsv1.StatefulSetSpec{
		PodManagementPolicy: appsv1.ParallelPodManagement,
		// UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
		// 	Type: appsv1.RollingUpdateStatefulSetStrategyType,
		// 	RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
		// 		Partition:      nil,
		// 		MaxUnavailable: P(intstr.FromInt(1)),
		// 	},
		// },
		Replicas:    P(int32(N.replicas)),
		Selector:    &metav1.LabelSelector{MatchLabels: N.MatchLabels()},
		ServiceName: SVC.Name,
		VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
			{
				ObjectMeta: metav1.ObjectMeta{Name: N.PvcName},
				Spec: corev1.PersistentVolumeClaimSpec{
					AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
					Resources: corev1.VolumeResourceRequirements{
						Requests: map[corev1.ResourceName]resource.Quantity{
							corev1.ResourceName("storage"): resource.MustParse(ResourceStorage),
						},
					},
					StorageClassName: P(N.StorageClass),
				},
			},
		},
		Template: corev1.PodTemplateSpec{
			ObjectMeta: metav1.ObjectMeta{
				Annotations: ku.AnnotationPrometheus(
					ku.PathMetrics, N.Metrics.Container.ContainerPort,
				),
				Labels: N.MatchLabels(),
			},

			Spec: corev1.PodSpec{
				Affinity: PAA,
				Containers: []corev1.Container{
					{
						Name:  N.Name, // default container
						Image: N.Img.URL(),
						Command: []string{
							"nats-server", "--config", N.ConfigPath,
						},
						ImagePullPolicy: corev1.PullIfNotPresent,

						Env: []corev1.EnvVar{
							N.Config.HashEnv("CONFIG_HASH"),
							ku.EnvVarDownAPI("POD_NAME", "metadata.name"),
							ku.EnvVarDownAPI(
								"POD_NAMESPACE",
								"metadata.namespace",
							),
							{Name: "SERVER_NAME", Value: "$(POD_NAME)"},
							{Name: "GOMEMLIMIT", Value: ResourceMemory + "B"},
							{
								Name:  "CLUSTER_ADVERTISE",
								Value: "$(POD_NAME).nats.$(POD_NAMESPACE).svc.cluster.local",
							},
							// cm.HashEnv("CONFIG_HASH_ENV"), // no need since we have the config-reloader
						},

						Lifecycle: preStop(
							"nats-server", "-sl=ldm="+N.pidPath,
						),

						LivenessProbe:  probe,
						ReadinessProbe: probe,
						StartupProbe:   startupProbe,

						Ports: []corev1.ContainerPort{
							N.Client.Container,
							N.Cluster.Container,
							N.Monitor.Container,
						},

						Resources: ku.Resources(
							d(ResourceCPU), ResourceMemory,
							d(ResourceCPU), ResourceMemory,
						),

						VolumeMounts: []corev1.VolumeMount{
							N.Config.VolumeMount,
							N.pidVM,
							{MountPath: N.storageDir, Name: N.PvcName},
						},
					},
					{
						Name:  "reloader",
						Image: N.ConfigReloader.URL(),
						Command: []string{
							"nats-server-config-reloader",
							"-pid", N.pidPath,
							"-config", N.ConfigPath,
						},
						ImagePullPolicy: corev1.PullIfNotPresent,
						VolumeMounts: []corev1.VolumeMount{
							N.Config.VolumeMount,
							N.pidVM,
						},
					},
					{
						Name:            "promexporter",
						Image:           N.PromExporter.URL(),
						ImagePullPolicy: corev1.PullIfNotPresent,
						Args: []string{
							// see https://github.com/nats-io/prometheus-nats-exporter/blob/main/main.go#L87
							// "-connz", // connection metrics
							"-connz_detailed",         // advanced connection metrics
							"-jsz=all",                // jetstream metrics
							"-routez",                 // route metrics
							"-subz",                   // subscription metrics
							"-varz",                   // general metrics
							"-prefix=nats",            // prefix for all metrics
							"-use_internal_server_id", // using serverID from /varz
							"http://localhost:" + d(N.Monitor.Container.ContainerPort) + "/",
						},
						Ports: []corev1.ContainerPort{N.Metrics.Container},
					},
				},
				DNSPolicy:                     corev1.DNSClusterFirst,
				ServiceAccountName:            SA.Name,
				ShareProcessNamespace:         P(true), // necessary for the config reloader
				TerminationGracePeriodSeconds: P(int64(60)),
				Volumes: []corev1.Volume{
					N.Config.VolumeAndMount().Volume(),
					{
						Name:         N.pidVM.Name,
						VolumeSource: corev1.VolumeSource{},
					},
				},
			},
		},
	},
}

func preStop(cmd ...string) *corev1.Lifecycle {
	return &corev1.Lifecycle{
		PreStop: &corev1.LifecycleHandler{
			Exec: &corev1.ExecAction{Command: cmd},
		},
	}
}

var probe = &corev1.Probe{
	FailureThreshold:    int32(3),
	InitialDelaySeconds: int32(10),
	PeriodSeconds:       int32(30),
	ProbeHandler: ku.ProbeHTTP(
		"/",
		int(N.Monitor.Container.ContainerPort),
	),
	SuccessThreshold: int32(1),
	TimeoutSeconds:   int32(5),
}

var startupProbe = &corev1.Probe{
	FailureThreshold:    int32(90),
	InitialDelaySeconds: int32(10),
	PeriodSeconds:       int32(10),
	ProbeHandler: ku.ProbeHTTP(
		ku.PathHealthz,
		int(N.Monitor.Container.ContainerPort),
	),

	SuccessThreshold: int32(1),
	TimeoutSeconds:   int32(5),
}
