package common

import (
	"context"
	"fmt"
	"time"

	"helm.sh/helm/v3/pkg/chart"
	"helm.sh/helm/v3/pkg/chart/loader"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/network/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/controllers/common"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/helm"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

const (
	defaultMemLimit = "256Mi"
	defaultMemReq   = "80Mi"
	defaultCPUReq   = "20m"
)

type Proxy struct {
	*task.BaseTask
	config *ProxyConfig
}

type ProxyConfig struct {
	helmChartPath string

	proxyImage    config.ImageConfig
	gottyImage    config.ImageConfig
	allowedCidrs  []string
	auth0Audience string
	auth0Domain   string
	network       *cloudv1.Network
	resources     config.Resources
	enableDevMode bool
}

func (c *ProxyConfig) GetNamespace() string {
	return task.AdminNamespace
}

func (c *ProxyConfig) GetName() string {
	return "proxy"
}

func (c *ProxyConfig) ToHelmChart() (result *chart.Chart, err error) {
	result, err = loader.Load(c.helmChartPath)
	if err != nil {
		return nil, fmt.Errorf("failed to load helm chart %q: %w", c.helmChartPath, err)
	}
	return
}

func (c *ProxyConfig) ToHelmValues() map[string]interface{} {
	service := map[string]interface{}{
		"type": "ClusterIP",
	}

	limits := map[string]string{
		// Do not set cpu limits, see: https://home.robusta.dev/blog/stop-using-cpu-limits
		"memory": defaultMemLimit,
	}
	if len(c.resources.MemoryLimits) > 0 {
		limits["memory"] = c.resources.MemoryLimits
	}
	requests := map[string]string{
		"cpu":    defaultCPUReq,
		"memory": defaultMemReq,
	}
	if len(c.resources.CPURequests) > 0 {
		requests["cpu"] = c.resources.CPURequests
	}
	if len(c.resources.MemoryRequests) > 0 {
		requests["memory"] = c.resources.MemoryRequests
	}

	// TODO: disable cluster proxy lb once it's not used
	service["type"] = "LoadBalancer"
	tags := common.ConcatenateTags(
		cloudv1.TagsFromLabels(c.network.Labels),
		map[string]string{
			common.Component: "dbaas-proxy",
			common.UsedBy:    "dbaas-control-plane-shoot",
		})
	service["annotations"] = map[string]string{
		"service.beta.kubernetes.io/aws-load-balancer-scheme": "internet-facing",
		// `external` indicates it's managed by out-of-tree lb-controller
		// see https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.4/guide/service/annotations/#traffic-routing
		"service.beta.kubernetes.io/aws-load-balancer-type":                     "external",
		"service.beta.kubernetes.io/aws-load-balancer-nlb-target-type":          "instance",
		"service.beta.kubernetes.io/aws-load-balancer-target-group-attributes":  "preserve_client_ip.enabled=true",
		"service.beta.kubernetes.io/aws-load-balancer-additional-resource-tags": cloudv1.TagStringForAWS(tags),
	}
	if len(c.allowedCidrs) == 0 {
		// Using default CIDR
		service["loadBalancerSourceRanges"] = []string{"0.0.0.0/0"}
	} else {
		service["loadBalancerSourceRanges"] = c.allowedCidrs
	}

	return map[string]interface{}{
		"replicaCount": 2,
		"env": []interface{}{
			map[string]string{
				"name":  "AUTH0-AUDIENCE",
				"value": c.auth0Audience,
			},
			map[string]string{
				"name":  "AUTH0-DOMAIN",
				"value": c.auth0Domain,
			},
		},
		"image": map[string]string{
			"repository": c.proxyImage.Repo,
			"tag":        c.proxyImage.Tag,
		},
		"gotty": map[string]string{
			"repository": c.gottyImage.Repo,
			"tag":        c.gottyImage.Tag,
		},
		"service": service,
		"resources": map[string]interface{}{
			"limits":   limits,
			"requests": requests,
		},
		"ingress": map[string]interface{}{
			"enabled": true,
		},
		"deployment": map[string]interface{}{
			"proxyArgs": map[string]interface{}{
				"enableDevMode": c.enableDevMode,
			},
		},
	}
}

func (c *ProxyConfig) GetCRDs() ([]*apiextensionsv1.CustomResourceDefinition, error) {
	return nil, nil
}

func NewProxy(ctx *task.Context) *Proxy {
	return &Proxy{
		BaseTask: &task.BaseTask{Context: ctx},
		config: &ProxyConfig{
			helmChartPath: ctx.Config.Network.Proxy.ChartPath,
			proxyImage:    ctx.Config.Network.Proxy.ProxyImage,
			gottyImage:    ctx.Config.Network.Proxy.GottyImage,
			allowedCidrs:  ctx.Config.Network.Proxy.AllowedCidrs,
			auth0Audience: ctx.Config.Network.Proxy.Auth0Audience,
			auth0Domain:   ctx.Config.Network.Proxy.Auth0Domain,
			network:       ctx.Network,
			resources:     ctx.Config.Network.Proxy.Resources,
			enableDevMode: ctx.Config.Network.Proxy.EnableDevMode,
		},
	}
}

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

func (t *Proxy) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	return deploy.ErrorOrDone(helm.Apply(ctx, t.Remote, t.config))
}

func (t *Proxy) PostCreate(ctx context.Context) (res deploy.Result) {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}
	var service corev1.Service
	if err := cli.Get(ctx, types.NamespacedName{Namespace: t.config.GetNamespace(), Name: t.config.GetName() + "-cluster-proxy"}, &service); err != nil {
		return deploy.Error(err)
	}

	var ingress networkingv1.Ingress
	if err := cli.Get(ctx, types.NamespacedName{Namespace: t.config.GetNamespace(), Name: t.config.GetName() + "-cluster-proxy"}, &ingress); err != nil {
		return deploy.Error(err)
	}

	if config.FeatureEnabled(t.Config.FeatureGates, config.UseProxyGatewayIngress, t.GetTenantID(), t.GetProjectID()) {
		// if cluster gateway feature gate is enabled, the service name is clustergateway-kong-proxy, use the cluster gateway lb for the base url
		if err := eksv1alpha1.UpdateNetworkStatus(ctx, t.Local, t.Network, func(status *eksv1alpha1.CommonNetworkStatus) error {
			status.ProxyBaseUrl = status.GatewayBaseUrl
			return nil
		}); err != nil {
			return deploy.Error(fmt.Errorf("failed to update status for proxy base url"))
		}
		return deploy.Done()
	}

	if service.Spec.Type == corev1.ServiceTypeLoadBalancer {
		if len(service.Status.LoadBalancer.Ingress) < 1 {
			return deploy.Error(fmt.Errorf("waiting for proxy load balancer be provisioned"))
		}
		var addr string
		if t.Network.Spec.Provider == consts.ProviderTypeEKS {
			addr = service.Status.LoadBalancer.Ingress[0].Hostname
		} else if t.Network.Spec.Provider == consts.ProviderTypeGKE {
			addr = service.Status.LoadBalancer.Ingress[0].IP
		}

		if err = eksv1alpha1.UpdateNetworkStatus(ctx, t.Local, t.Network, func(status *eksv1alpha1.CommonNetworkStatus) error {
			status.ProxyBaseUrl = addr
			return nil
		}); err != nil {
			return deploy.Error(fmt.Errorf("failed to update status for proxy base url"))
		}
	}

	return deploy.Done()
}

// Delete removes the proxy service, must use deploy.Panic() if error is not null.
// Because we want to remove the service before other tasks(eks,nodegroup,vpc).
func (t *Proxy) Delete(ctx context.Context) deploy.Result {
	deleted, err := ensureProxyServiceDeleted(t.Network)
	if err != nil {
		return deploy.Panic(err)
	}
	if deleted {
		return deploy.Done()
	}

	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return deploy.Done()
		}
		return deploy.Panic(err)
	}
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.config.GetName() + "-cluster-proxy",
			Namespace: t.config.GetNamespace(),
		},
	}

	err = cli.Delete(ctx, svc)
	if err != nil {
		// if the service is not found, we can think the loadbalancer is deleted
		if apierrors.IsNotFound(err) {
			err = eksv1alpha1.UpdateNetworkStatus(ctx, t.Local, t.Network, func(status *eksv1alpha1.CommonNetworkStatus) error {
				status.ProxyBaseUrl = ""
				return nil
			})
			if err != nil {
				return deploy.Panic(fmt.Errorf("failed to update network status proxy base url to empty"))
			}
			return deploy.Done()
		}
		return deploy.Panic(err)
	}
	return deploy.Panic(fmt.Errorf("waiting for proxy service to be deleted"))
}

func ensureProxyServiceDeleted(network *cloudv1.Network) (deleted bool, err error) {
	status, err := v1alpha1.EKSNetworkStatusFromNetwork(network)
	if err != nil {
		return false, err
	}
	if status.ProxyBaseUrl == "" {
		return true, nil
	}
	return false, nil
}
