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"
	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 (
	CLUSTER_GATEWAY_VALUE_TEMPLATE = `
image:
  repository: kong
  tag: "3.6"
ingressController:
  image:
    repository: kong/kubernetes-ingress-controller
    tag: "3.1"
replicaCount: 2
admin:
  enabled: true
  http:
    enabled: true
  type: ClusterIP
proxy:
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-additional-resource-tags: tags
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: preserve_client_ip.enabled=true
    service.beta.kubernetes.io/aws-load-balancer-type: external
  enabled: true
  loadBalancerSourceRanges:
    - 0.0.0.0/0
  externalTrafficPolicy: Local
  type: LoadBalancer
`
)

type ClusterGateway struct {
	*task.BaseTask
	config *ClusterGatewayConfig
}

type ClusterGatewayConfig struct {
	helmChartPath string

	allowedCidrs               []string
	crdPaths                   []string
	network                    *cloudv1.Network
	kongImage                  config.ImageConfig
	kongIngressControllerImage config.ImageConfig
	resources                  config.Resources
}

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

func (c *ClusterGatewayConfig) GetName() string {
	return "clustergateway"
}

func (c *ClusterGatewayConfig) 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 *ClusterGatewayConfig) ToHelmValues() map[string]interface{} {
	values, err := helm.LoadValuesFromYAML(CLUSTER_GATEWAY_VALUE_TEMPLATE)
	if err != nil {
		return nil
	}

	// use the correct tags for loadbalancer
	tags := common.ConcatenateTags(
		cloudv1.TagsFromLabels(c.network.Labels),
		map[string]string{
			common.Component: "cluster-gateway",
			common.UsedBy:    "dbaas-control-plane-shoot",
		})

	proxy, ok := values["proxy"].(map[string]interface{})
	if ok {
		annotations, ok := proxy["annotations"].(map[string]interface{})
		if ok {
			annotations["service.beta.kubernetes.io/aws-load-balancer-additional-resource-tags"] = cloudv1.TagStringForAWS(tags)
		}
	}

	image, ok := values["image"].(map[string]interface{})
	if ok {
		image["repository"] = c.kongImage.Repo
		image["tag"] = c.kongImage.Tag
	}

	ingressController, ok := values["ingressController"].(map[string]interface{})
	if ok {
		image, ok := ingressController["image"].(map[string]interface{})
		if ok {
			image["repository"] = c.kongIngressControllerImage.Repo
			image["tag"] = c.kongIngressControllerImage.Tag
		}
	}

	if len(c.allowedCidrs) != 0 {
		if proxy, ok := values["proxy"].(map[string]interface{}); ok {
			proxy["loadBalancerSourceRanges"] = c.allowedCidrs
		}
	}

	return values
}

func (c *ClusterGatewayConfig) GetCRDs() ([]*apiextensionsv1.CustomResourceDefinition, error) {
	var result []*apiextensionsv1.CustomResourceDefinition
	for _, path := range c.crdPaths {
		crd, err := helm.LoadCRD(path)
		if err != nil {
			return nil, err
		}
		result = append(result, crd...)
	}
	return result, nil
}

func NewClusterGateway(ctx *task.Context) *ClusterGateway {
	return &ClusterGateway{
		BaseTask: &task.BaseTask{Context: ctx},
		config: &ClusterGatewayConfig{
			helmChartPath:              ctx.Config.Network.ClusterGateway.ChartPath,
			crdPaths:                   ctx.Config.Network.ClusterGateway.CRDPaths,
			allowedCidrs:               ctx.Config.Network.ClusterGateway.AllowedCidrs,
			network:                    ctx.Network,
			kongImage:                  ctx.Config.Network.ClusterGateway.KongImage,
			kongIngressControllerImage: ctx.Config.Network.ClusterGateway.KongIngressControllerImage,
			resources:                  ctx.Config.Network.ClusterGateway.Resources,
		},
	}
}

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

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

func (t *ClusterGateway) 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: "tidb-admin", Name: "clustergateway-kong-proxy"}, &service); err != nil {
		return deploy.Error(err)
	}
	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
		}
		err = eksv1alpha1.UpdateNetworkStatus(ctx, t.Local, t.Network, func(status *eksv1alpha1.CommonNetworkStatus) error {
			status.GatewayBaseUrl = addr
			return nil
		})
		if 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 *ClusterGateway) Delete(ctx context.Context) deploy.Result {
	deleted, err := ensureClusterGatewayServiceDeleted(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{
			Namespace: "tidb-admin",
			Name:      "clustergateway-kong-proxy",
		},
	}

	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.GatewayBaseUrl = ""
				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 ensureClusterGatewayServiceDeleted(network *cloudv1.Network) (deleted bool, err error) {
	status, err := v1alpha1.EKSNetworkStatusFromNetwork(network)
	if err != nil {
		return false, err
	}
	if status.GatewayBaseUrl == "" {
		return true, nil
	}
	return false, nil
}
