package main

import (
	"context"
	"encoding/csv"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"sort"
	"strconv"
	"time"

	"github.com/google/uuid"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	cloudclient "github.com/tidbcloud/infra-api/cmd/scaler/client"
)

const (
	StatusCreating  = "CREATING"
	StatusAvailable = "AVAILABLE"

	passPrefix = "first-pass-when-creating.cloud.pingcap.com/"
)

type Item struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Status string `json:"status"`
}

type Clusters struct {
	Total int    `json:"total"`
	Items []Item `json:"items"`
}

type PostResp struct {
	ID string `json:"id"`
}

type testInfo struct {
	totalCnt int
	paral    int
	project  int
}

type Status struct {
	// IDs tracks the IDs of the clusters created during the test
	IDs []int `json:"ids"`
	// Suffix is the test session suffix
	Suffix string `json:"suffix"`
}

func main() {
	var (
		session string
		info    testInfo

		autoclean  bool
		skipcreate bool
		kubeconfig string
		pubkey     string
		privkey    string
		host       string
	)
	flag.StringVar(&session, "session", "", "test session name")
	flag.IntVar(&info.totalCnt, "total", 100, "number of total clusters to be created")
	flag.IntVar(&info.paral, "paral", 5, "parallism")
	flag.IntVar(&info.project, "project", 223467, "project id")
	flag.StringVar(&kubeconfig, "kubeconfigpath", "", "path of kubeconfig")
	flag.StringVar(&pubkey, "pubkey", "", "TiDB cloud openapi public key")
	flag.StringVar(&privkey, "privkey", "", "TiDB cloud openapi private key")
	flag.StringVar(&host, "host", "https://api.dev.tidbcloud.com", "TiDB cloud openapi host")
	flag.BoolVar(&autoclean, "autoclean", true, "automatically cleanup test clusters")
	flag.BoolVar(&skipcreate, "skipcreate", false, "skip cluster creation")
	flag.Parse()

	cc := cloudclient.NewDedicatedClient(host, strconv.Itoa(info.project), pubkey, privkey)
	cli := getDefaultK8sClient(kubeconfig)

	status := new(Status)
	var recordPath string
	if session == "" {
		suffix := uuid.New().String()[:8]
		session = fmt.Sprintf("session-%s-%s", generateTimestamp(), suffix)
		recordPath = fmt.Sprintf("%s.json", session)
		status.Suffix = suffix
	} else {
		var err error
		recordPath = fmt.Sprintf("%s.json", session)
		status, err = loadStatus(recordPath)
		if err != nil {
			panic(fmt.Errorf("load status error: %s", err.Error()))
		}
	}
	fmt.Printf("Test session: %s\n", session)

	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("test duration: %s\n", elapsed.String())
		err := syncStatus(recordPath, status)
		if err != nil {
			panic(err)
		}
	}()

	if !skipcreate {
		ticker := time.Tick(time.Second)
		for range ticker {
			completed := reconcile(&info, status, cc)
			if completed {
				break
			}
		}

		// retrieve cluster info and write to file
		csvRecord := fmt.Sprintf("%s.csv", session)
		file, err := os.OpenFile(csvRecord, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640)
		if err != nil {
			panic(err)
		}
		defer file.Close()

		data, err := retrieveInfraClusterInfo(context.Background(), cli, status, &info)
		if err != nil {
			panic(err)
		}
		err = csv.NewWriter(file).WriteAll(data)
		if err != nil {
			panic(err)
		}
		fmt.Printf("===\n  completed creation for %d clusters with parallism %d\n", info.totalCnt, info.paral)
		fmt.Printf("  written cluster info to: %s\n", csvRecord)
	}

	if autoclean {
		clusters, err := cc.Get()
		// clusters, err := retrieveClusterInfo(context.Background(), &info)
		if err != nil {
			panic(fmt.Sprintf("retrieve cluster list during deletion err: %s", err.Error()))
		}
		for _, item := range clusters.Items {
			if err := cc.Delete(item.ID); err != nil {
				panic(fmt.Sprintf("delete cluster %s err: %s", item.ID, err.Error()))
			}
			fmt.Printf("deleted cluster %s, time: %s\n", item.ID, time.Now().String())
		}
		return
	}
}

func loadStatus(path string) (status *Status, err error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	status = new(Status)
	err = json.Unmarshal(data, status)
	if err != nil {
		return nil, err
	}

	return
}

func syncStatus(path string, status *Status) error {
	file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640)
	if err != nil {
		return err
	}
	defer file.Close()

	bytes, err := json.Marshal(status)
	if err != nil {
		return err
	}
	_, err = file.Write(bytes)
	if err != nil {
		fmt.Printf("Failed write status to file: %s\nContent:\n%s\n", err.Error(), string(bytes))
		return err
	}

	return nil
}

func classifyClusters(clusters *cloudclient.Clusters) (creating, normal []int) {
	for _, item := range clusters.Items {
		idInt, _ := strconv.Atoi(item.ID)
		if item.Status.ClusterStatus == StatusCreating {
			creating = append(creating, idInt)
		} else if item.Status.ClusterStatus == StatusAvailable {
			normal = append(normal, idInt)
		}
	}
	return
}

func reconcile(info *testInfo, status *Status, cc cloudclient.Client) (completed bool) {
	// retrieve cluster information
	clusters, err := cc.Get()
	if err != nil {
		fmt.Printf("retrieve cluster info err: %s\n", err.Error())
	}
	// filter unrelevant clusters
	creating, normal := classifyClusters(clusters)

	// check total count reached
	if len(status.IDs) >= info.totalCnt {
		return len(filterClusters(normal, status)) == info.totalCnt
	}
	// check parallism reached
	if len(creating) >= info.paral {
		return
	}
	// create cluster
	clusterSeq := len(status.IDs) + 1
	clusterID, err := cc.Create(fmt.Sprintf("cluster-%d-%s", clusterSeq, status.Suffix))
	if err != nil {
		fmt.Printf("create cluster err: %s\n", err.Error())
	}
	if id, err := strconv.Atoi(clusterID); err == nil {
		status.IDs = append(status.IDs, id)
	}
	fmt.Printf("created cluster %s, time: %s\n", clusterID, time.Now().String())
	return
}

func filterClusters(ids []int, status *Status) []int {
	tracked := make(map[int]bool)
	for _, id := range status.IDs {
		tracked[id] = true
	}

	var filtered []int
	for _, id := range ids {
		if tracked[id] {
			filtered = append(filtered, id)
		}
	}
	return filtered
}

func generateTimestamp() string {
	return time.Now().Format("2006-01-02-15-04-05")
}

func getDefaultK8sClient(kubeconfig string) client.Client {
	if kubeconfig == "" {
		kubeconfig = os.Getenv("KUBECONFIG")
	}
	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	utilruntime.Must(err)
	scheme := runtime.NewScheme()
	utilruntime.Must(cloudv1.SchemeBuilder.AddToScheme(scheme))
	cli, err := client.New(config, client.Options{Scheme: scheme})
	utilruntime.Must(err)
	return cli
}

func retrieveInfraClusterInfo(ctx context.Context, cli client.Client, status *Status, info *testInfo) ([][]string, error) {
	clusters := &cloudv1.ClusterList{}
	// list clusters by namespace
	err := cli.List(ctx, clusters, client.InNamespace(fmt.Sprintf("nightly-ms-%d", info.project)))
	if err != nil {
		return nil, err
	}
	// retrieve columns info
	columnsIdx := map[string]int{}
	columns := []string{}
	for _, cluster := range clusters.Items {
		for key := range cluster.Annotations {
			if len(key) <= len(passPrefix) || key[:len(passPrefix)] != passPrefix {
				continue
			}
			key = key[len(passPrefix):]
			columnsIdx[key] = 0
		}
	}
	for key := range columnsIdx {
		columns = append(columns, key)
	}
	sort.Strings(columns)
	columns = append([]string{"id"}, columns...)
	for i, key := range columns {
		columnsIdx[key] = i
	}

	// retrieve cluster info
	res := [][]string{columns}
	for _, cluster := range clusters.Items {
		row := make([]string, len(columnsIdx))
		row[0] = cluster.Name
		for key, value := range cluster.Annotations {
			if len(key) <= len(passPrefix) || key[:len(passPrefix)] != passPrefix {
				continue
			}
			key = key[len(passPrefix):]
			durationSeconds, _ := convertTimestamp(value)
			row[columnsIdx[key]] = durationSeconds
		}
		res = append(res, row)
	}
	return res, nil
}

// convertTimestamp converts the string of time.Duration to numeric string representing seconds.
func convertTimestamp(s string) (string, error) {
	d, err := time.ParseDuration(s)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%f", d.Seconds()), nil
}
