package common

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/samber/lo"

	"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"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/utils"
	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/deploy"
	"github.com/tidbcloud/infra-provider/pkg/helm"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

const (
	regionalKongIngressClass = "kong"
	dataplaneKongProxyPort   = 80
	defaultMinPendingAge     = time.Minute * 5
)

// ClusterAgent is the task to deploy a Deployment supporting execute pd-ctl/tikv-ctl
type ClusterAgent struct {
	*task.BaseTask
	config *ClusterAgentConfig
}

var _ deploy.Task = &ClusterAgent{}

func NewClusterAgent(ctx *task.Context) deploy.Task {
	return &ClusterAgent{
		BaseTask: &task.BaseTask{Context: ctx},
		config: &ClusterAgentConfig{
			agentConf:           ctx.Config.ClusterController.ClusterAgent,
			cluster:             ctx.Cluster,
			tikvImage:           "", // fill it later
			pdImage:             "", // fill it later
			pdPatrolImage:       "", // fill it later
			dryRun:              ctx.Config.DryRun.Enabled,
			tidbOperatorVersion: "v1",
		},
	}
}

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

func (t *ClusterAgent) Create(ctx context.Context) (res deploy.Result) {
	// When the cluster is suspended, the v2 getter can not get the image since there are no pods.
	if t.Cluster.Status.Phase == cloudv1.ClusterPhaseSuspend {
		t.Logger.Info("the cluster is suspended, skip this task")
		return deploy.Done()
	}

	if t.TCGetter == nil || t.TCGetter.IsNil() {
		return deploy.Fatal(fmt.Errorf("tidb cluster is not exist yet"))
	}

	t.BeginAt = util.Pointer(time.Now())
	t.config.tikvImage = t.TCGetter.TiKVImage()
	t.config.pdImage = t.TCGetter.PDImage()
	if t.config.tikvImage == "" || t.config.pdImage == "" {
		return deploy.Error(fmt.Errorf("can't find tikv or pd image"))
	}

	// Use pdImage url for pd-patrol-image url by replacing version to SHA digests.
	t.config.pdPatrolImage = genPatrolUrlWithPdImage(t.config.pdImage, t.config.agentConf.PDPatrolImage.Tag)

	if t.Cluster.FeatureEnabled(consts.ClusterFeatureUseTiDBOperatorV2) {
		t.config.tidbOperatorVersion = "v2"
	}

	var applyOpts []helm.ApplyOption
	if t.config.agentConf.AutoUnlockPending {
		minPendingAge := t.config.agentConf.AutoUnlockMinPendingAge.Duration
		if minPendingAge < defaultMinPendingAge {
			minPendingAge = defaultMinPendingAge
		}
		applyOpts = append(applyOpts, helm.WithAutoUnlockPending(minPendingAge))
	}
	if err := helm.Apply(ctx, helm.NewClientGetterWrapper(t.EnvProvider), t.config, applyOpts...); err != nil {
		return deploy.Error(err)
	}
	if err := t.createIngressInRegional(ctx); err != nil {
		return deploy.Error(fmt.Errorf("create cluster-agent ingress in regional k8s: %w", err))
	}
	return deploy.Done()
}

func genPatrolUrlWithPdImage(pdImage string, tag string) string {
	if pdImage == "" || tag == "" {
		return ""
	}

	// Index image tag like "pingcap/pd:v7.5.1"
	// Trim possible tag (after :)
	image := pdImage
	if symbolColon := strings.LastIndex(image, ":"); symbolColon != -1 {
		image = image[:symbolColon]
	}

	return fmt.Sprintf("%s:%s", image, tag)
}

func (t *ClusterAgent) PostCreate(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *ClusterAgent) Delete(ctx context.Context) deploy.Result {
	if err := t.deleteIngressInRegional(ctx); err != nil {
		return deploy.Error(fmt.Errorf("delete cluster-agent ingress in regional k8s: %w", err))
	}

	return deploy.ErrorOrDone(helm.Uninstall(ctx, helm.NewClientGetterWrapper(t.EnvProvider), t.config, t.config.GetName()))
}

func (t *ClusterAgent) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

// createIngressInRegional register the dataplane cluster-agent into regional Kong
func (t *ClusterAgent) createIngressInRegional(ctx context.Context) error {
	// ignore for Cloud Native Arch clusters
	if !t.IsRegionalManaged() {
		return nil
	}

	pathType := networkingv1.PathTypePrefix
	pathPrefix := fmt.Sprintf("/cluster-agent/%s", t.TCGetter.Namespace())
	remoteCli := t.EnvProvider.GetClient()
	dataplaneKongHostname, err := utils.GetDataplaneKongLBHostname(ctx, remoteCli)
	if err != nil {
		return fmt.Errorf("get dataplane kong LB hostname: %w", err)
	}
	labels := map[string]string{
		cloudv1.LabelServiceType: cloudv1.ServiceTypeDedicated,
		cloudv1.TenantTagKey:     t.Cluster.GetTenantID(),
		cloudv1.ProjectTagKey:    t.Cluster.GetProjectID(),
		cloudv1.ClusterTagKey:    t.Cluster.GetClusterID(),
	}

	// this service is used to expose the dataplane cluster-agent to regional k8s
	service := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.config.GetName(),
			Namespace: t.Cluster.Namespace,
			Labels:    labels,
		},
		Spec: corev1.ServiceSpec{
			Type:         corev1.ServiceTypeExternalName,
			ExternalName: dataplaneKongHostname,
		},
	}
	if err := k8s.Apply(ctx, t.Local, service); err != nil {
		return fmt.Errorf("apply cluster-agent service in regional k8s: %w", err)
	}

	// NOTE: Although the region-server [is using HTTPRoute](https://github.com/tidbcloud/app-delivery/blob/main/dedicated-regional-server/regional-server/base/httproute.yaml)
	// instead of Ingress, we still use Ingress for convenience.
	// register dataplane cluster-agent into regional Kong
	ingress := &networkingv1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.config.GetName(),
			Namespace: t.Cluster.Namespace,
			Labels:    labels,
			Annotations: map[string]string{
				"konghq.com/methods":   "GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD, TRACE",
				"konghq.com/protocols": "http",
				// don't strip path prefix here but keep the prefix and strip it in dataplane Kong
				// "konghq.com/strip-path": "true",
			},
		},
		Spec: networkingv1.IngressSpec{
			IngressClassName: ptr.To(regionalKongIngressClass),
			Rules: []networkingv1.IngressRule{
				{
					IngressRuleValue: networkingv1.IngressRuleValue{
						HTTP: &networkingv1.HTTPIngressRuleValue{
							Paths: []networkingv1.HTTPIngressPath{
								{
									Backend: networkingv1.IngressBackend{
										Service: &networkingv1.IngressServiceBackend{
											Name: service.Name,
											Port: networkingv1.ServiceBackendPort{
												Number: dataplaneKongProxyPort,
											},
										},
									},
									Path:     pathPrefix,
									PathType: &pathType,
								},
							},
						},
					},
				},
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, ingress); err != nil {
		return fmt.Errorf("apply cluster-agent ingress in regional k8s: %w", err)
	}
	return nil
}

func (t *ClusterAgent) deleteIngressInRegional(ctx context.Context) error {
	// ignore for Cloud Native Arch clusters
	if !t.IsRegionalManaged() {
		return nil
	}

	ingress := &networkingv1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: t.config.GetName(), Namespace: t.Cluster.Namespace}}
	if err := t.Local.Delete(ctx, ingress); client.IgnoreNotFound(err) != nil {
		return err
	}

	service := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: t.config.GetName(), Namespace: t.Cluster.Namespace}}
	if err := t.Local.Delete(ctx, service); client.IgnoreNotFound(err) != nil {
		return err
	}

	return nil
}

type ClusterAgentConfig struct {
	agentConf           config.ClusterAgentConfig
	cluster             *cloudv1.Cluster
	tikvImage           string
	pdImage             string
	pdPatrolImage       string
	tidbOperatorVersion string
	dryRun              bool
}

var _ helm.DryRunablePackage = &ClusterAgentConfig{}

func (c *ClusterAgentConfig) GetNamespace() string {
	return c.cluster.PrefixedId()
}

func (c *ClusterAgentConfig) GetName() string {
	name := fmt.Sprintf("cluster-agent-%s", c.cluster.Name)
	return name
}

func (c *ClusterAgentConfig) DryRun() bool {
	return c.dryRun
}

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

func (c *ClusterAgentConfig) ToHelmValues() map[string]interface{} {
	limits := map[string]string{
		// Do not set cpu limits, see: https://home.robusta.dev/blog/stop-using-cpu-limits
		"memory": lo.Must(lo.Coalesce(c.agentConf.Resources.MemoryLimits, "500Mi")),
	}

	requests := map[string]string{
		"cpu":    lo.Must(lo.Coalesce(c.agentConf.Resources.CPURequests, "20m")),
		"memory": lo.Must(lo.Coalesce(c.agentConf.Resources.MemoryRequests, "20Mi")),
	}

	agentArgs := map[string]interface{}{
		"provider":            c.cluster.GetCloudProvider(),
		"tidbOperatorVersion": c.tidbOperatorVersion,
	}
	if c.cluster.GetProviderType() == cloudv1.ProviderTypeGKE {
		projectID := strings.TrimSpace(c.cluster.Spec.Kubernetes.Account)
		agentArgs["gkePolicyControlProjectID"] = projectID
		agentArgs["gkePolicyControlLocation"] = strings.TrimSpace(c.cluster.GetRegion())
		agentArgs["gkePolicyControlClusterName"] = strings.TrimSpace(c.cluster.Spec.Kubernetes.Name)
		agentArgs["gkePolicyControlFleetProjectID"] = projectID
	}

	return map[string]interface{}{
		"replicaCount": 1,
		"clusterAgent": map[string]interface{}{
			"image": map[string]interface{}{
				"repository": c.agentConf.AgentImage.Repo,
				"tag":        c.agentConf.AgentImage.Tag,
			},
			"args": agentArgs,
		},
		"serviceAccount": map[string]interface{}{
			"name": namer.ClusterAgentServiceAccountName(c.cluster.Name),
		},
		"tikvCtl": map[string]interface{}{
			"imageStr": c.tikvImage,
		},
		"pdCtl": map[string]interface{}{
			"imageStr": c.pdImage,
		},
		"pdPatrolCtl": map[string]interface{}{
			"imageStr": c.pdPatrolImage,
		},
		"resources": map[string]interface{}{
			"limits":   limits,
			"requests": requests,
		},
		"ingress": map[string]interface{}{
			"enabled": true,
		},
	}
}

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