package parent

import (
	"context"
	"fmt"
	"sort"

	vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	prometheusv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
	"github.com/tidbcloud/observability/pkg/common/configs"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/deploy"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/serverless/task"

	k8sapiv1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	observabilitypingcapcomv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
)

const (
	vmAlertDefaultSpecID           = "vmalert.default"
	vmAlertExtraArgLoggerMaxArgLen = "10000"
	vmAlertRemoteWriteMaxBatchSize = int32(5000)
	vmSelectURLTemplate            = "http://vmselect-%s.observability.svc.cluster.local:8481/select/%d:%d/prometheus"
	vmInsertURLTemplate            = "http://vminsert-%s.observability.svc.cluster.local:8480/insert/%d:%d/prometheus"
	alertManagerPort               = 9093
	alertManagerURL                = "http://%s.%s.%s.svc.cluster.local:%d"
	sidecarCPURequest              = "10m"
)

type VMAlert struct {
	*task.BaseTask
}

func NewVMAlert(ctx *task.Context) *VMAlert {
	return &VMAlert{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *VMAlert) Name() string {
	return "VMAlert"
}

func (t *VMAlert) Create(ctx context.Context) deploy.Result {
	if t.Remote == nil {
		return deploy.Panic(fmt.Errorf("remote client is not initialized"))
	}
	if t.Cluster == nil {
		return deploy.Panic(fmt.Errorf("cluster context is nil"))
	}

	needVM, err := t.vmAlertNeeded(ctx, t.Cluster)
	if err != nil {
		return deploy.Error(err)
	}

	if !needVM {
		if err := t.ensureVMAlertDeleted(ctx); err != nil {
			return deploy.Error(err)
		}
		return deploy.Done()
	}

	desiredObj, err := t.makeVMAlert(ctx, t.Cluster)
	if err != nil {
		return deploy.Error(err)
	}

	desiredAlert, ok := desiredObj.(*vmv1beta1.VMAlert)
	if !ok {
		return deploy.Error(fmt.Errorf("unexpected vmalert type %T", desiredObj))
	}

	alert := &vmv1beta1.VMAlert{ObjectMeta: metav1.ObjectMeta{
		Name:      desiredAlert.GetName(),
		Namespace: desiredAlert.GetNamespace(),
	}}

	if _, err := controllerutil.CreateOrUpdate(ctx, t.Remote, alert, func() error {
		alert.SetLabels(desiredAlert.GetLabels())
		alert.SetAnnotations(desiredAlert.GetAnnotations())
		alert.Spec = desiredAlert.Spec
		return nil
	}); err != nil {
		return deploy.Error(err)
	}

	return deploy.Done()
}

func (t *VMAlert) Delete(ctx context.Context) deploy.Result {
	if t.Remote == nil {
		return deploy.Panic(fmt.Errorf("remote client is not initialized"))
	}
	if t.Cluster == nil {
		return deploy.Panic(fmt.Errorf("cluster context is nil"))
	}

	if err := t.ensureVMAlertDeleted(ctx); err != nil {
		return deploy.Error(err)
	}

	return deploy.Done()
}

func (t *VMAlert) ensureVMAlertDeleted(ctx context.Context) error {
	alert := &vmv1beta1.VMAlert{}
	key := client.ObjectKey{Namespace: t.Cluster.Namespace, Name: t.Cluster.Name}
	if err := t.Remote.Get(ctx, key, alert); err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}
	if err := t.Remote.Delete(ctx, alert); err != nil && !apierrors.IsNotFound(err) {
		return err
	}
	return nil
}

func (t *VMAlert) vmAlertNeeded(ctx context.Context, cluster *observabilitypingcapcomv1.Cluster) (bool, error) {
	if cluster.Spec.MuteAlert {
		return false, nil
	}

	bizType := cluster.Labels[constants.LabelBizType]
	appID := cluster.Labels[constants.LabelApplicationID]
	if !configs.MultiTenancyRuleEnabled(bizType, appID) {
		return true, nil
	}

	vmRule := &vmv1beta1.VMRule{}
	if err := t.Remote.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, vmRule); err != nil {
		if apierrors.IsNotFound(err) {
			return true, nil
		}
		return false, err
	}

	if vmRule.Labels[constants.LabelMultiTenancyRule] != "true" {
		return true, nil
	}
	return false, nil
}

func (t *VMAlert) makeVMAlert(ctx context.Context, cluster *observabilitypingcapcomv1.Cluster) (client.Object, error) {
	alert := &vmv1beta1.VMAlert{}
	alert.SetNamespace(cluster.Namespace)
	alert.SetName(cluster.Name)
	alert.SetLabels(cluster.Labels)
	alert.SetAnnotations(cluster.Annotations)

	// now we not set sa to vmalert

	if cluster.Spec.VictoriaAccount.Alerting != nil {
		notifiers, err := t.buildNotifiers(ctx, cluster.Spec.VictoriaAccount.Alerting)
		if err != nil {
			return nil, fmt.Errorf("build notifiers failed: %w", err)
		}
		alert.Spec.Notifiers = notifiers
	}

	alert.Spec.ExtraArgs = map[string]string{
		"loggerMaxArgLen": vmAlertExtraArgLoggerMaxArgLen,
	}

	alert.Spec.Datasource = vmv1beta1.VMAlertDatasourceSpec{
		URL: fmt.Sprintf(vmSelectURLTemplate,
			cluster.Spec.VictoriaAccount.VMClusterName,
			cluster.Spec.VictoriaAccount.AccountID,
			cluster.Spec.VictoriaAccount.ProjectID,
		),
	}

	alert.Spec.RemoteRead = &vmv1beta1.VMAlertRemoteReadSpec{
		URL: fmt.Sprintf(vmSelectURLTemplate,
			cluster.Spec.VictoriaAccount.VMClusterName,
			cluster.Spec.VictoriaAccount.AccountID,
			cluster.Spec.VictoriaAccount.ProjectID,
		),
	}

	alert.Spec.RemoteWrite = &vmv1beta1.VMAlertRemoteWriteSpec{
		URL: fmt.Sprintf(vmInsertURLTemplate,
			cluster.Spec.VictoriaAccount.VMClusterName,
			cluster.Spec.VictoriaAccount.AccountID,
			cluster.Spec.VictoriaAccount.ProjectID,
		),
		MaxBatchSize: ptr.To(vmAlertRemoteWriteMaxBatchSize),
	}

	specID := cluster.Annotations[constants.AnnotationMonitorSpec]
	if len(specID) == 0 {
		specID = vmAlertDefaultSpecID
	}
	spec := t.Config.MonitorSpec[specID]

	if cluster.Spec.VictoriaAccount.MaxQueueSize != nil {
		alert.Spec.RemoteWrite.MaxQueueSize = cluster.Spec.VictoriaAccount.MaxQueueSize
	} else if spec.MaxQueueSize != nil {
		alert.Spec.RemoteWrite.MaxQueueSize = spec.MaxQueueSize
	}

	alert.Spec.RuleSelector = cluster.Spec.VictoriaAccount.RuleSelector
	alert.Spec.ReplicaCount = ptr.To(int32(1))
	alert.Spec.Resources.Requests = make(k8sapiv1.ResourceList)
	alert.Spec.Resources.Limits = make(k8sapiv1.ResourceList)

	resources := cluster.Spec.VictoriaAccount.AlertResources
	if resources.Requests != nil {
		if mem := resources.Requests.Memory(); mem != nil {
			alert.Spec.Resources.Requests[k8sapiv1.ResourceMemory] = *mem
		}
		if cpu := resources.Requests.Cpu(); cpu != nil {
			alert.Spec.Resources.Requests[k8sapiv1.ResourceCPU] = *cpu
		}
	} else if container, ok := configs.GetSpecContainer()[specID]; ok {
		alert.Spec.Resources.Requests = container.Requests.DeepCopy()
	}

	if resources.Limits != nil {
		if mem := resources.Limits.Memory(); mem != nil {
			alert.Spec.Resources.Limits[k8sapiv1.ResourceMemory] = *mem
		}
		if cpu := resources.Limits.Cpu(); cpu != nil {
			alert.Spec.Resources.Limits[k8sapiv1.ResourceCPU] = *cpu
		}
	} else if container, ok := configs.GetSpecContainer()[specID]; ok {
		alert.Spec.Resources.Limits = container.Limits.DeepCopy()
	}

	alert.Spec.ExternalLabels = make(map[string]string)
	if cluster.Labels != nil {
		alert.Spec.ExternalLabels[constants.ExternalLabelBizType] = cluster.Labels[constants.LabelBizType]
		alert.Spec.ExternalLabels[constants.ExternalLabelTenantID] = cluster.Labels[constants.LabelTenantID]
		alert.Spec.ExternalLabels[constants.ExternalLabelApplicationID] = cluster.Labels[constants.LabelApplicationID]
		alert.Spec.ExternalLabels[constants.ExternalLabelProjectID] = cluster.Labels[constants.LabelProjectID]
		alert.Spec.ExternalLabels[constants.ExternalLabelO11yID] = cluster.Name
		alert.Spec.ExternalLabels[constants.ExternalLabelRegion] = fmt.Sprintf("%s-%s", cluster.Labels[constants.LabelVendor], cluster.Labels[constants.LabelRegion])
	}

	alert.Spec.Containers = append(alert.Spec.Containers, overrideContainer("config-reloader"))

	alert.Spec.NodeSelector = map[string]string{
		"node_type":                   "worker",
		"topology.kubernetes.io/zone": cluster.Spec.VictoriaAccount.AvailabilityZone,
	}

	alert.Spec.EmbeddedProbes = &vmv1beta1.EmbeddedProbes{
		LivenessProbe:  &k8sapiv1.Probe{InitialDelaySeconds: 30},
		ReadinessProbe: &k8sapiv1.Probe{InitialDelaySeconds: 30},
	}

	return alert, nil
}

func (t *VMAlert) buildNotifiers(ctx context.Context, alerting *prometheusv1.AlertingSpec) ([]vmv1beta1.VMAlertNotifierSpec, error) {
	if alerting == nil || len(alerting.Alertmanagers) == 0 {
		return nil, nil
	}

	alertManager := alerting.Alertmanagers[0]
	notifiers := make([]vmv1beta1.VMAlertNotifierSpec, 0)

	defaultNotifier := func() vmv1beta1.VMAlertNotifierSpec {
		return vmv1beta1.VMAlertNotifierSpec{
			URL: fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", alertManager.Name, alertManager.Namespace, alertManagerPort),
		}
	}

	svc := &k8sapiv1.Service{ObjectMeta: metav1.ObjectMeta{Name: alertManager.Name, Namespace: alertManager.Namespace}}
	if err := t.Remote.Get(ctx, client.ObjectKeyFromObject(svc), svc); err != nil {
		if apierrors.IsNotFound(err) {
			notifiers = append(notifiers, defaultNotifier())
			return notifiers, nil
		}
		return nil, err
	}

	port := alertManagerPort
	for _, p := range svc.Spec.Ports {
		if p.Name == alertManager.Name {
			port = int(p.Port)
			break
		}
	}

	podList := &k8sapiv1.PodList{}
	if err := t.Remote.List(ctx, podList, &client.ListOptions{
		LabelSelector: labels.SelectorFromSet(svc.Spec.Selector),
		Namespace:     alertManager.Namespace,
	}); err != nil {
		return nil, err
	}

	if len(podList.Items) == 0 {
		notifiers = append(notifiers, defaultNotifier())
		return notifiers, nil
	}

	for _, pod := range podList.Items {
		notifiers = append(notifiers, vmv1beta1.VMAlertNotifierSpec{
			URL: fmt.Sprintf(alertManagerURL, pod.Name, alertManager.Name, alertManager.Namespace, port),
		})
	}
	//sort list to make the notifiers stable
	sort.Slice(notifiers, func(i, j int) bool {
		return notifiers[i].URL < notifiers[j].URL
	})

	return notifiers, nil
}

func overrideContainer(name string) k8sapiv1.Container {
	container := k8sapiv1.Container{Name: name}
	container.Resources.Requests = make(k8sapiv1.ResourceList)
	container.Resources.Limits = make(k8sapiv1.ResourceList)
	cpuQuantity := resource.MustParse(sidecarCPURequest)
	container.Resources.Requests[k8sapiv1.ResourceCPU] = cpuQuantity
	container.Resources.Limits[k8sapiv1.ResourceCPU] = cpuQuantity
	return container
}
