package common

import (
	"context"
	"fmt"

	"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"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/controllers/common"

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

type DataflowAgent struct {
	*task.BaseTask
	config *DataflowAgentConfig
}

type DataflowAgentConfig struct {
	fullManager   bool
	helmChartPath string
	image         config.ImageConfig
	network       *cloudv1.Network
	allowedCidrs  []string
}

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

func (c *DataflowAgentConfig) GetName() string {
	return "dataflow-agent"
}

func (c *DataflowAgentConfig) 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 *DataflowAgentConfig) ToHelmValues() map[string]interface{} {
	service := map[string]interface{}{
		"type": "ClusterIP",
	}
	if c.fullManager {
		service["type"] = "LoadBalancer"
		tags := common.ConcatenateTags(
			cloudv1.TagsFromLabels(c.network.Labels),
			map[string]string{
				common.Component: "dataflow-agent",
				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": 1,
		"image": map[string]string{
			"repository": c.image.Repo,
			"tag":        c.image.Tag,
		},
		"service": service,
	}
}

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

func NewDataflowAgent(ctx *task.Context) *DataflowAgent {
	return &DataflowAgent{
		BaseTask: &task.BaseTask{Context: ctx},
		config: &DataflowAgentConfig{
			fullManager:   true,
			helmChartPath: ctx.Config.Network.DataflowAgent.HelmConfig.ChartPath,
			image:         ctx.Config.Network.DataflowAgent.DataflowAgentImage,
			allowedCidrs:  ctx.Config.Network.DataflowAgent.AllowedCidrs,
			network:       ctx.Network,
		},
	}
}

func (t *DataflowAgent) Name() string {
	return "dataflow-agent"
}

func (t *DataflowAgent) Create(ctx context.Context) deploy.Result {
	return deploy.ErrorOrDone(helm.Apply(ctx, t.Remote, t.config))
}

func (t *DataflowAgent) 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()}, &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 dataflow-agent 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.DataFlowAgentURL = addr
			return nil
		})
		if err != nil {
			return deploy.Error(fmt.Errorf("failed to update status for dataflow-agent base url"))
		}
	}
	return deploy.Done()
}

// Delete removes the dataflow-agent 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 *DataflowAgent) Delete(ctx context.Context) deploy.Result {
	deleted, err := ensureDataFlowAgentServiceDeleted(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(),
			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.DataFlowAgentURL = ""
				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 dataflow-agent service %s/%s to be deleted", svc.Namespace, svc.Name))
}

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