package argocd

import (
	"bytes"
	"context"
	_ "embed"
	"encoding/json"
	"fmt"
	"slices"
	"text/template"
	"time"

	"github.com/juju/errors"
	log "github.com/sirupsen/logrus"
	k8s_errors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	applyv1 "k8s.io/client-go/applyconfigurations/core/v1"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"

	"github.com/tidbcloud/auto-deploy/internal/constants"
	"github.com/tidbcloud/auto-deploy/internal/types"
	"github.com/tidbcloud/auto-deploy/internal/utils"
)

//go:embed argocd_kubeconfig.yml.tpl
var argoKubeconfigTemplate string

const ARGOCD_NAMESPACE = "argocd"

func RegisterK8s(k8s *types.K8s, overwriteArgocdSecret bool) error {
	var err error
	var sec *applyv1.SecretApplyConfiguration
	switch k8s.Region.CloudProviderType {
	case types.AWS:
		sec, err = genK8sArgocdSecret(k8s)
	case types.Azure:
		sec, err = genAzureK8sArgocdSecret(k8s)
	case types.AliCloud:
		sec, err = genAliCloudK8sArgocdSecret(k8s)
	default:
		return errors.NotImplementedf("Unsupported cloud provider %s", k8s.Region.CloudProviderType)
	}
	if err != nil {
		return err
	}

	clientset, err := getArgocdK8sClientSet(k8s.Region.InstanceName)
	if err != nil {
		return err
	}
	client := clientset.CoreV1().Secrets(ARGOCD_NAMESPACE)
	return applySecret(client, sec, overwriteArgocdSecret)
}

func applySecret(client corev1.SecretInterface, secret *applyv1.SecretApplyConfiguration, overwrite bool) error {
	ctx := context.TODO()
	oldSecret, err := client.Get(ctx, *secret.Name, metav1.GetOptions{})
	if err != nil && !k8s_errors.IsNotFound(err) {
		return errors.Annotatef(err, "Failed to get k8s secret '%s'", *secret.Name)
	}

	if err == nil && oldSecret != nil {
		uppdated := false
		for key, value := range secret.StringData {
			oldValue := oldSecret.Data[key]
			if !slices.Equal(oldValue, []byte(value)) {
				if !overwrite {
					log.WithFields(log.Fields{
						"secret":    *secret.Name,
						"key":       key,
						"old-value": string(oldValue),
						"new-value": value,
					}).Error("The secret is not updated due to overwrite is false")
					return errors.AlreadyExistsf("k8s secret %s", *secret.Name)
				} else {
					uppdated = true
					log.WithFields(log.Fields{
						"secret":    *secret.Name,
						"key":       key,
						"old-value": string(oldValue),
						"new-value": value,
					}).Info("Overwrite k8s secret")
				}
			}
		}

		if !uppdated {
			log.WithField("name", *secret.Name).Info("Sercret is identical, no need to update")
			return nil
		}
	}

	_, err = client.Apply(ctx, secret, metav1.ApplyOptions{
		FieldManager: constants.K8S_APPLY_FIELD_MANAGER,
	})
	if err == nil {
		log.WithFields(log.Fields{
			"secret": *secret.Name,
		}).Info("Applied k8s secret")
	} else {
		log.WithFields(log.Fields{
			"secret": *secret.Name,
			"err":    err,
		}).Info("Failed to apply k8s secret")
	}
	return err
}

func UnregisterK8s(k8s *types.K8s) error {
	config, err := getArgocdK8sConfig(k8s.Region.InstanceName)
	if err != nil {
		return err
	}

	// wait all applications deleted
	dynamicClient, err := dynamic.NewForConfig(config)
	if err != nil {
		return err
	}
	err = waitAllApplicationsDeleted(dynamicClient, k8s.Name)
	if err != nil {
		return err
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		return err
	}
	client := clientset.CoreV1().Secrets(ARGOCD_NAMESPACE)

	err = client.Delete(context.TODO(), k8s.Name, metav1.DeleteOptions{})
	if err == nil {
		log.WithFields(log.Fields{
			"k8s-name": k8s.Name,
		}).Info("Delete argocd k8s secret")
	} else if k8s_errors.IsNotFound(err) {
		err = nil // ignore not found error
		log.WithFields(log.Fields{
			"k8s-name": k8s.Name,
		}).Info("Argocd k8s secret is not found")
	} else {
		log.WithFields(log.Fields{
			"k8s-name": k8s.Name,
			"err":      err,
		}).Error("Failed to delete argocd k8s secret")
	}
	return err
}

func waitAllApplicationsDeleted(dynamicClient *dynamic.DynamicClient, k8sName string) error {
	for {
		apps, err := listArgocdApplications(dynamicClient, k8sName)
		if err != nil {
			return err
		}
		if len(apps) == 0 {
			return nil
		}
		log.WithFields(log.Fields{
			"k8s-name": k8sName,
			"apps":     apps,
		}).Info("Wait all argocd applications deleted")
		time.Sleep(time.Second * 10)
	}
}

func listArgocdApplications(dynamicClient *dynamic.DynamicClient, k8sName string) ([]string, error) {
	// ArgoCD application CR
	gvr := schema.GroupVersionResource{
		Group:    "argoproj.io",
		Version:  "v1alpha1",
		Resource: "applications",
	}

	crList, err := dynamicClient.Resource(gvr).Namespace(ARGOCD_NAMESPACE).List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		if k8s_errors.IsNotFound(err) {
			return []string{}, nil
		}
		return nil, err
	}

	apps := make([]string, 0, 10)
	for _, item := range crList.Items {
		val, found, _ := unstructured.NestedString(item.Object, "spec", "destination", "name")
		if found && val == k8sName {
			apps = append(apps, item.GetName())
		}
	}
	return apps, nil
}

func genK8sArgocdSecret(k8s *types.K8s) (*applyv1.SecretApplyConfiguration, error) {
	ca, err := utils.RunCommandAndGetOutput("./scripts/get-eks-info.sh", "ca", k8s.Name, k8s.Region.Region)
	if err != nil {
		return nil, err
	}
	utils.MaskSensitiveGithubOutput(ca)
	log.WithFields(log.Fields{
		"k8s-name": k8s.Name,
		"ca":       ca,
	}).Debug("Get k8s ca")

	config, _ := json.Marshal(map[string]any{
		"execProviderConfig": map[string]any{
			"command": "argocd-k8s-auth",
			"args": []string{
				"aws",
				"--cluster-name",
				k8s.Name,
			},
			"env": map[string]string{
				"AWS_ROLE_ARN": fmt.Sprintf(
					"arn:aws:iam::%s:role/argocd-oidc-role",
					k8s.Region.Account.AwsRole.AccountId,
				),
			},
			"apiVersion": "client.authentication.k8s.io/v1beta1",
		},
		"tlsClientConfig": map[string]any{
			"insecure": false,
			"caData":   ca,
		},
	})
	log.WithFields(log.Fields{
		"k8s-name": k8s.Name,
		"config":   string(config),
	}).Debug("Get k8s argocd config")

	sec := applyv1.Secret(k8s.Name, ARGOCD_NAMESPACE).WithLabels(map[string]string{
		"argocd.argoproj.io/secret-type": "cluster",
	}).WithStringData(map[string]string{
		"name":   k8s.Name,
		"server": k8s.EndPoint,
		"config": string(config),
	})
	return sec, nil
}

func genAzureK8sArgocdSecret(k8s *types.K8s) (*applyv1.SecretApplyConfiguration, error) {
	config, err := utils.RunCommandAndGetOutput("./scripts/get-aks-info.sh", "argocd-config", k8s.Name)
	if err != nil {
		return nil, err
	}

	type tlsClientConfig struct {
		Insecure bool   `json:"insecure"`
		CertData string `json:"certData"`
		KeyData  string `json:"keyData"`
		CaData   string `json:"caData"`
	}
	type azureK8sConfig struct {
		BearerToken     string `json:"bearerToken"`
		TlsClientConfig tlsClientConfig
	}
	var conf azureK8sConfig
	err = json.Unmarshal([]byte(config), &conf)
	if err != nil {
		return nil, errors.Annotatef(err, "Failed to unmarshal k8s config")
	}

	utils.MaskSensitiveGithubOutput(conf.BearerToken)
	utils.MaskSensitiveGithubOutput(conf.TlsClientConfig.CertData)
	utils.MaskSensitiveGithubOutput(conf.TlsClientConfig.KeyData)
	utils.MaskSensitiveGithubOutput(conf.TlsClientConfig.CaData)
	log.WithFields(log.Fields{
		"k8s-name": k8s.Name,
		"config":   string(config),
	}).Debug("Get k8s argocd config")

	sec := applyv1.Secret(k8s.Name, ARGOCD_NAMESPACE).WithLabels(map[string]string{
		"argocd.argoproj.io/secret-type": "cluster",
	}).WithStringData(map[string]string{
		"name":   k8s.Name,
		"server": k8s.EndPoint,
		"config": config,
	})
	return sec, nil
}

func genAliCloudK8sArgocdSecret(k8s *types.K8s) (*applyv1.SecretApplyConfiguration, error) {
	config, err := utils.RunCommandAndGetOutput("./scripts/get-ack-info.sh", "argocd-config", k8s.Name)
	if err != nil {
		return nil, err
	}

	type tlsClientConfig struct {
		Insecure bool   `json:"insecure"`
		CertData string `json:"certData"`
		KeyData  string `json:"keyData"`
		CaData   string `json:"caData"`
	}
	type alicloudK8sConfig struct {
		TlsClientConfig tlsClientConfig
	}
	var conf alicloudK8sConfig
	err = json.Unmarshal([]byte(config), &conf)
	if err != nil {
		return nil, errors.Annotatef(err, "Failed to unmarshal k8s config")
	}

	utils.MaskSensitiveGithubOutput(conf.TlsClientConfig.CertData)
	utils.MaskSensitiveGithubOutput(conf.TlsClientConfig.KeyData)
	utils.MaskSensitiveGithubOutput(conf.TlsClientConfig.CaData)
	log.WithFields(log.Fields{
		"k8s-name": k8s.Name,
		"config":   string(config),
	}).Debug("Get k8s argocd config")

	sec := applyv1.Secret(k8s.Name, ARGOCD_NAMESPACE).WithLabels(map[string]string{
		"argocd.argoproj.io/secret-type": "cluster",
	}).WithStringData(map[string]string{
		"name":   k8s.Name,
		"server": k8s.EndPoint,
		"config": config,
	})
	return sec, nil
}

func getArgocdK8sConfig(env string) (*rest.Config, error) {
	argoCluster := "cicd-cluster-us-central1"
	if env != "prod" {
		argoCluster = fmt.Sprintf("%s-%s", env, argoCluster)
	}
	argoRegion := "us-central1"
	argoEndpoint, err := utils.RunCommandAndGetOutputIgnoreStderr(
		"gcloud", true, "container", "clusters", "describe", argoCluster,
		"--region", argoRegion, "--format", "value(endpoint)",
	)
	if err != nil {
		return nil, err
	}
	log.WithField("endpoint", argoEndpoint).Debug("Get argocd k8s endpoint")

	argoCA, err := utils.RunCommandAndGetOutputIgnoreStderr(
		"gcloud", true, "container", "clusters", "describe", argoCluster,
		"--region", argoRegion, "--format", "value(masterAuth.clusterCaCertificate)",
	)
	if err != nil {
		return nil, err
	}
	utils.MaskSensitiveGithubOutput(argoCA)
	log.WithField("ca", argoCA).Debug("Get argocd k8s ca")

	tmpl, err := template.New("*").Parse(argoKubeconfigTemplate)
	if err != nil {
		return nil, err
	}
	kubeconfig := bytes.NewBuffer([]byte{})
	if err := tmpl.Execute(kubeconfig, map[string]any{
		"region":   argoRegion,
		"cluster":  argoCluster,
		"endpoint": argoEndpoint,
		"ca":       argoCA,
	}); err != nil {
		return nil, err
	}
	log.WithField("kubeconfig", kubeconfig).Debug("Get argocd k8s kubeconfig")

	return clientcmd.RESTConfigFromKubeConfig(kubeconfig.Bytes())
}

func getArgocdK8sClientSet(env string) (*kubernetes.Clientset, error) {
	config, err := getArgocdK8sConfig(env)
	if err != nil {
		return nil, err
	}
	return kubernetes.NewForConfig(config)
}
