package parent

import (
	"context"
	"fmt"

	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/deploy"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/serverless/task"

	k8snetworkv1 "k8s.io/api/networking/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	svcPrefix                      = "svc-"
	serverlessPrefix               = "serverless-"
	readSuffix                     = "-read"
	openReadSuffix                 = "-read-open"
	vmInsertAPI                    = "/insert/"
	vmSelectAPI                    = "/select/"
	vmInsertPort             int32 = 8480
	vmSelectPort             int32 = 8481
	rewriteTargetTemplate          = "%s%d:%d/prometheus/$2"
	serverlessTargetTemplate       = "%smultitenant/prometheus/$2"
)

type ingressBuilder func() (*k8snetworkv1.Ingress, error)

type Ingress struct {
	*task.BaseTask
}

func NewIngress(ctx *task.Context) *Ingress {
	return &Ingress{BaseTask: &task.BaseTask{Context: ctx}}
}

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

func (t *Ingress) 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"))
	}

	builders := []ingressBuilder{
		t.makeVMSelectIngress,
		t.makeVMOpenSelectIngress,
		t.makeVMInsertIngress,
		t.makeServerlessInsertIngress,
	}

	for _, builder := range builders {
		desired, err := builder()
		if err != nil {
			return deploy.Error(err)
		}

		ingress := &k8snetworkv1.Ingress{ObjectMeta: metav1.ObjectMeta{
			Name:      desired.GetName(),
			Namespace: desired.GetNamespace(),
		}}

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

	return deploy.Done()
}

func (t *Ingress) 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 t.Cluster.Annotations[constants.AnnotationShareMode] != string(constants.SHARED) {
		return deploy.Done()
	}

	names := []string{
		svcPrefix + t.Cluster.Spec.Endpoint.Name + readSuffix,
		svcPrefix + t.Cluster.Spec.Endpoint.Name + openReadSuffix,
		svcPrefix + t.Cluster.Spec.Endpoint.Name,
		serverlessPrefix + t.Cluster.Spec.Endpoint.Name,
	}

	for _, name := range names {
		ingress := &k8snetworkv1.Ingress{}
		key := client.ObjectKey{Namespace: t.Cluster.Namespace, Name: name}
		if err := t.Remote.Get(ctx, key, ingress); err != nil {
			if apierrors.IsNotFound(err) {
				continue
			}
			return deploy.Error(err)
		}

		if err := t.Remote.Delete(ctx, ingress); err != nil && !apierrors.IsNotFound(err) {
			return deploy.Error(err)
		}
	}

	return deploy.Done()
}

func (t *Ingress) makeVMSelectIngress() (*k8snetworkv1.Ingress, error) {
	az := t.Cluster.Spec.VictoriaAccount.AvailabilityZone
	azCfg, ok := t.Config.AZConfig[az]
	if !ok {
		return nil, fmt.Errorf("az %s not found in config", az)
	}

	sourcePrefix := fmt.Sprintf(constants.ReadMetricsInternalPathPrefix, t.Cluster.Name)

	desired := t.buildIngress(
		svcPrefix+t.Cluster.Spec.Endpoint.Name+readSuffix,
		azCfg.ReadIngressClassName,
		vmSelectAPI,
		sourcePrefix,
		vmSelectSvcPrefix+t.Cluster.Name,
		vmSelectPort,
		"",
		false,
	)

	return desired, nil
}

func (t *Ingress) makeVMOpenSelectIngress() (*k8snetworkv1.Ingress, error) {
	az := t.Cluster.Spec.VictoriaAccount.AvailabilityZone
	azCfg, ok := t.Config.AZConfig[az]
	if !ok {
		return nil, fmt.Errorf("az %s not found in config", az)
	}

	bizType := t.Cluster.Labels[constants.LabelBizType]
	bizCfg, ok := t.Config.BizTypeConfigs[bizType]
	if !ok {
		return nil, fmt.Errorf("biz type %s not found in config", bizType)
	}

	sourcePrefix := fmt.Sprintf(
		constants.ReadMetricsExternalPathPrefixTemplate,
		t.Cluster.Labels[constants.LabelBizType],
		t.Cluster.Labels[constants.LabelTenantID],
		t.Cluster.Labels[constants.LabelProjectID],
		t.Cluster.Labels[constants.LabelApplicationID],
	)

	authURL := fmt.Sprintf(bizCfg.ExternalApiAuthUrlTemplate, t.Cluster.Labels[constants.LabelTenantID])

	desired := t.buildIngress(
		svcPrefix+t.Cluster.Spec.Endpoint.Name+openReadSuffix,
		azCfg.ReadIngressClassName,
		vmSelectAPI,
		sourcePrefix,
		vmSelectSvcPrefix+t.Cluster.Name,
		vmSelectPort,
		authURL,
		false,
	)

	return desired, nil
}

func (t *Ingress) makeVMInsertIngress() (*k8snetworkv1.Ingress, error) {
	az := t.Cluster.Spec.VictoriaAccount.AvailabilityZone
	azCfg, ok := t.Config.AZConfig[az]
	if !ok {
		return nil, fmt.Errorf("az %s not found in config", az)
	}

	sourcePrefix := fmt.Sprintf(constants.WriteMetricsPathPrefix, t.Cluster.Name)

	desired := t.buildIngress(
		svcPrefix+t.Cluster.Spec.Endpoint.Name,
		azCfg.IngressClassName,
		vmInsertAPI,
		sourcePrefix,
		vmInsertSvcPrefix+t.Cluster.Name,
		vmInsertPort,
		"",
		false,
	)

	return desired, nil
}

func (t *Ingress) makeServerlessInsertIngress() (*k8snetworkv1.Ingress, error) {
	az := t.Cluster.Spec.VictoriaAccount.AvailabilityZone
	azCfg, ok := t.Config.AZConfig[az]
	if !ok {
		return nil, fmt.Errorf("az %s not found in config", az)
	}

	sourcePrefix := fmt.Sprintf(constants.ServerlessWriteMetricsPathPrefix, t.Cluster.Name)

	desired := t.buildIngress(
		serverlessPrefix+t.Cluster.Spec.Endpoint.Name,
		azCfg.IngressClassName,
		vmInsertAPI,
		sourcePrefix,
		vmInsertSvcPrefix+t.Cluster.Name,
		vmInsertPort,
		"",
		true,
	)

	return desired, nil
}

func (t *Ingress) buildIngress(name, ingressClass, targetPrefix, sourcePath, targetService string, targetPort int32, authURL string, serverless bool) *k8snetworkv1.Ingress {
	ingress := &k8snetworkv1.Ingress{}
	ingress.SetName(name)
	ingress.SetNamespace(t.Cluster.Namespace)
	ingress.Spec.IngressClassName = pointer.String(ingressClass)

	annotations := map[string]string{
		"nginx.ingress.kubernetes.io/use-regex": "true",
	}

	if authURL != "" {
		annotations["nginx.ingress.kubernetes.io/auth-url"] = authURL
		annotations["nginx.ingress.kubernetes.io/auth-cache-key"] = "$remote_user$http_authorization"
		annotations["nginx.ingress.kubernetes.io/enable-cors"] = "true"
		annotations["nginx.ingress.kubernetes.io/cors-allow-origin"] = "*"
		annotations["nginx.ingress.kubernetes.io/cors-allow-methods"] = "GET"
	}

	if serverless {
		annotations["nginx.ingress.kubernetes.io/rewrite-target"] = fmt.Sprintf(serverlessTargetTemplate, targetPrefix)
	} else {
		annotations["nginx.ingress.kubernetes.io/rewrite-target"] = fmt.Sprintf(
			rewriteTargetTemplate,
			targetPrefix,
			t.Cluster.Spec.VictoriaAccount.AccountID,
			t.Cluster.Spec.VictoriaAccount.ProjectID,
		)
	}

	ingress.SetAnnotations(annotations)

	pathType := k8snetworkv1.PathTypeImplementationSpecific
	ingress.Spec.Rules = []k8snetworkv1.IngressRule{{
		IngressRuleValue: k8snetworkv1.IngressRuleValue{
			HTTP: &k8snetworkv1.HTTPIngressRuleValue{
				Paths: []k8snetworkv1.HTTPIngressPath{{
					Path:     sourcePath + "(/|$)(.*)",
					PathType: &pathType,
					Backend: k8snetworkv1.IngressBackend{
						Service: &k8snetworkv1.IngressServiceBackend{
							Name: targetService,
							Port: k8snetworkv1.ServiceBackendPort{Number: targetPort},
						},
					},
				}},
			},
		},
	}}

	return ingress
}
