package main

import (
	"context"
	"crypto/tls"
	"database/sql"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/crossplane-contrib/provider-gcp/apis"
	"github.com/go-sql-driver/mysql"
	"github.com/google/uuid"
	"gopkg.in/yaml.v2"

	apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/clientcmd/api"
	v1 "k8s.io/client-go/tools/clientcmd/api/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
)

const (
	CONTROL_PLANE = "controlplane"
	DATA_PLANE    = "dataplane"
)

var (
	mykubeconfig string
	configPath   string
)

type Config struct {
	DB struct {
		User     string `yaml:"user"`
		Password string `yaml:"password"`
		Host     string `yaml:"host"`
		Port     string `yaml:"port"`
		Name     string `yaml:"name"`
	} `yaml:"db"`
	K8s struct {
		CRDs struct {
			ControlPlane []string `yaml:"controlplaneCRDs"`
			DataPlane    []string `yaml:"dataplaneCRDs"`
		} `yaml:"crds"`
		Region string `yaml:"region"`
		Source string `yaml:"source"`
	} `yaml:"k8s"`
}

func loadConfig(filePath string) (*Config, error) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return nil, err
	}
	var config Config
	err = yaml.Unmarshal(data, &config)
	if err != nil {
		return nil, err
	}
	if envPassword := os.Getenv("DB_PASSWORD"); envPassword != "" {
		config.DB.Password = envPassword
	}
	log.Printf("Loaded config: %+v", config)
	return &config, nil
}

func init() {

	flag.StringVar(&configPath, "configPath", "/etc/meta-backup/config.yaml", "path to the config file")
	flag.StringVar(&mykubeconfig, "mykubeconfig", "", "absolute path to the kubeconfig file")
}

func saveToK8sResourceDB(ctx context.Context, db *sql.DB, resourceType string, namespace string, name string, labels map[string]string, region string, source string, data string, snap_id string, plane string) error {

	tenantID := labels["tags.tidbcloud.com/cluster"]
	projectID := labels["tags.tidbcloud.com/project"]
	clusterID := labels["tags.tidbcloud.com/cluster"]
	query := `
	    INSERT INTO k8s_resource (type, namespace, name, tenant_id, project_id, cluster_id, region, source, data, snap_id, plane)
	    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
	`

	_, err := db.ExecContext(ctx, query, resourceType, namespace, name, tenantID, projectID, clusterID, region, source, data, snap_id, plane)
	if err != nil {
		return fmt.Errorf("failed to save to k8s_resource table: %w", err)
	}

	log.Printf("Data saved to k8s_resource: type=%s, namespace=%s, name=%s, snap_id=%s", resourceType, namespace, name, snap_id)
	return nil
}
func registerTLSConfig(host string) error {

	err := mysql.RegisterTLSConfig("tidb", &tls.Config{
		MinVersion: tls.VersionTLS12,
		ServerName: host,
	})
	if err != nil {
		return fmt.Errorf("failed to register TLS config: %w", err)
	}
	return nil
}

func main() {

	flag.Parse()
	// load config
	config, err := loadConfig(configPath)
	if err != nil {
		log.Fatalf("Failed to load config: %v", err)
	}

	if err := registerTLSConfig(config.DB.Host); err != nil {
		log.Fatalf("Failed to register TLS config: %v", err)
	}

	// build DSN
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?tls=tidb",
		config.DB.User,
		config.DB.Password,
		config.DB.Host,
		config.DB.Port,
		config.DB.Name,
	)

	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Failed to connect to MySQL: %v", err)
	}
	defer db.Close()

	region := config.K8s.Region
	source := config.K8s.Source

	var k8sconfig *rest.Config

	if mykubeconfig != "" {
		k8sconfig, err = clientcmd.BuildConfigFromFlags("", mykubeconfig)
	} else {
		k8sconfig, err = rest.InClusterConfig()
	}
	if err != nil {
		log.Fatalf("Failed to build Kubernetes config: %v", err)
	}
	clientset, err := kubernetes.NewForConfig(k8sconfig)
	if err != nil {
		log.Fatalf("Failed to create Kubernetes client: %v", err)
	}

	apiextensionsClientset, err := apiextensionsclient.NewForConfig(k8sconfig)
	if err != nil {
		log.Fatalf("Failed to create API extensions client: %v", err)
	}

	ctx := context.Background()

	controlPlaneCRDNames := config.K8s.CRDs.ControlPlane
	dataPlaneCRDNames := config.K8s.CRDs.DataPlane
	log.Printf("CRD Names: %v", controlPlaneCRDNames)
	log.Printf("CRD Names: %v", dataPlaneCRDNames)

	snap_id := uuid.New().String()
	processCRDs(ctx, db, apiextensionsClientset, clientset, controlPlaneCRDNames, region, source, snap_id, CONTROL_PLANE)

	s := runtime.NewScheme()
	if err := scheme.AddToScheme(s); err != nil {
		log.Fatalf("Failed to add Kubernetes core API types to scheme: %v", err)
	}
	if err := v1alpha1.AddToScheme(s); err != nil {
		log.Fatalf("Failed to add custom API types to scheme: %v", err)
	}
	if err := apis.AddToScheme(s); err != nil {
		log.Fatalf("Failed to add custom API types to scheme: %v", err)

	}

	cli, err := client.New(k8sconfig, client.Options{Scheme: s}) // get the control-plane k8s client
	if err != nil {
		log.Fatalf("Failed to create controller-runtime client: %v", err)
	}

	appBoxes := &v1alpha1.APPBoxList{}
	err = cli.List(ctx, appBoxes)
	if err != nil {
		log.Printf("Error listing APPBoxes: %v", err)
		return
	}
	for _, appBox := range appBoxes.Items {

		dataPlaneAPIExtensionsClient, dataPlaneClientset, err := getDataPlaneConfig(ctx, cli, &appBox, s)
		if err != nil {
			log.Printf("Error create data plane client for APPBox %s: %v", appBox.Name, err)
			continue
		}
		processCRDs(ctx, db, dataPlaneAPIExtensionsClient, dataPlaneClientset, dataPlaneCRDNames, region, appBox.Status.KubernetesClusterName, snap_id, DATA_PLANE)

	}

	log.Println("Backup process completed.")
}

func processCRDs(ctx context.Context, db *sql.DB, crdClient apiextensionsclient.Interface, clientset *kubernetes.Clientset, crdNames []string, region string, source string, snap_id string, plane string) {
	for _, crdName := range crdNames {
		crd, err := crdClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crdName, metav1.GetOptions{})
		log.Printf("Getting CRD %s", crdName)
		if err != nil {
			log.Printf("Error getting CRD %s: %v", crdName, err)
			continue
		}

		namespaces, err := clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
		if err != nil {
			log.Printf("Error listing namespaces: %v", err)
			continue
		}

		log.Printf("Processing CRD: %s", crd.Name)

		isClusterScoped := strings.ToLower(string(crd.Spec.Scope)) == "cluster"

		if isClusterScoped {
			data, err := json.Marshal(crd)
			if err != nil {
				log.Printf("Error marshalling CRD %s: %v", crd.Name, err)
				continue
			}

			err = saveToK8sResourceDB(ctx, db, crd.Name, "cluster-scoped", crd.Name, map[string]string{}, region, source, string(data), snap_id, plane)
			if err != nil {
				log.Printf("Failed to save to k8s_resource table: %v", err)
			}
			continue
		}

		for _, ns := range namespaces.Items {
			resourceList, err := clientset.RESTClient().Get().
				AbsPath("/apis", crd.Spec.Group, crd.Spec.Versions[0].Name, "namespaces", ns.Name, crd.Spec.Names.Plural).
				DoRaw(ctx)
			if err != nil {
				log.Printf("Error getting resources for CRD %s in namespace %s: %v", crd.Name, ns.Name, err)
				continue
			}

			var tempItems []json.RawMessage
			err = json.Unmarshal(resourceList, &struct {
				Items *[]json.RawMessage `json:"items"`
			}{Items: &tempItems})
			if err != nil {
				log.Printf("Error unmarshalling resource list for CRD %s in namespace %s: %v, raw data: %s", crd.Name, ns.Name, err, string(resourceList))
				continue
			}

			log.Printf("Number of items found for CRD %s in namespace %s: %d", crd.Name, ns.Name, len(tempItems))

			for _, rawItem := range tempItems {
				var metadata struct {
					Metadata struct {
						Name      string            `json:"name"`
						Namespace string            `json:"namespace"`
						Labels    map[string]string `json:"labels"`
					} `json:"metadata"`
				}
				err = json.Unmarshal(rawItem, &metadata)
				if err != nil {
					log.Printf("Error extracting metadata for resource in CRD %s in namespace %s: %v, raw data: %s", crd.Name, ns.Name, err, string(rawItem))
					continue
				}

				err = saveToK8sResourceDB(ctx, db, crd.Name, metadata.Metadata.Namespace, metadata.Metadata.Name, metadata.Metadata.Labels, region, source, string(rawItem), snap_id, plane)
				if err != nil {
					log.Printf("Failed to save resource to database for CRD %s in namespace %s: %v", crd.Name, ns.Name, err)
				}
			}
		}
	}
}
func getDataPlaneConfig(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox, s *runtime.Scheme) (apiextensionsclient.Interface, *kubernetes.Clientset, error) {
	namespacedName := types.NamespacedName{
		Namespace: appBox.Namespace,
		Name:      appBox.Name,
	}

	dataPlaneK8SConfig, err := controllers.ConfigFromAPPBox(ctx, cli, appBox, s)
	if err != nil {
		log.Printf("Error getting dataplanek8sconfig for APPBox %s: %v", namespacedName, err)
		return nil, nil, err
	}
	log.Printf("DataPlane K8S config host for APPBox %s: %s", namespacedName, dataPlaneK8SConfig.Host)

	// ensure the dataPlaneK8SConfig is using gke-gcloud-auth-plugin
	if dataPlaneK8SConfig.ExecProvider != nil && dataPlaneK8SConfig.ExecProvider.Command == "gke-gcloud-auth-plugin" {
		// has been used gke-gcloud-auth-plugin
	} else {
		// get information of gke cluster from appBox, and use it to generate kubeconfig using gke-gcloud-auth-plugin
		configV1 := v1.Config{
			Clusters: []v1.NamedCluster{
				{
					Name: appBox.Name,
					Cluster: v1.Cluster{
						Server: dataPlaneK8SConfig.Host,
					},
				},
			},
			AuthInfos: []v1.NamedAuthInfo{
				{
					Name: appBox.Name,
					AuthInfo: v1.AuthInfo{
						Exec: &v1.ExecConfig{

							Command: "gke-gcloud-auth-plugin",
							Args:    []string{"--use_application_default_credentials"},
							Env:     []v1.ExecEnvVar{},
						},
					},
				},
			},
			Contexts: []v1.NamedContext{
				{
					Name: appBox.Name,
					Context: v1.Context{
						Cluster:  appBox.Name,
						AuthInfo: appBox.Name,
					},
				},
			},
			CurrentContext: appBox.Name,
		}
		// transform v1.Config to api.Config
		var rawConfig api.Config
		data, err := json.Marshal(configV1)
		if err != nil {
			log.Printf("Error marshaling v1.Config for APPBox %s: %v", namespacedName, err)
			return nil, nil, err
		}
		if err := json.Unmarshal(data, &rawConfig); err != nil {
			log.Printf("Error unmarshaling to api.Config for APPBox %s: %v", namespacedName, err)
			return nil, nil, err
		}
		// use rawConfig create k8s config
		dataPlaneK8SConfig, err = clientcmd.NewDefaultClientConfig(rawConfig, &clientcmd.ConfigOverrides{}).ClientConfig()
		if err != nil {
			log.Printf("Error creating client config for APPBox %s: %v", namespacedName, err)
			return nil, nil, err
		}
	}
	dataPlaneClientset, err := kubernetes.NewForConfig(dataPlaneK8SConfig)
	if err != nil {
		log.Printf("Error creating data plane clientset for APPBox %s: %v", namespacedName, err)
		return nil, nil, err
	}

	dataPlaneAPIExtensionsClient, err := apiextensionsclient.NewForConfig(dataPlaneK8SConfig)
	if err != nil {
		log.Printf("Error creating API extensions client for APPBox %s: %v", namespacedName, err)
		return nil, nil, err
	}

	return dataPlaneAPIExtensionsClient, dataPlaneClientset, nil
}
