package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"hash/fnv"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/tidwall/gjson"
	"github.com/tidwall/sjson"
)

//type dashboardsData = map[string]json.RawMessage

var (
	sourcePath      string
	destinationPath string
	nextGen         bool
)

var variablesRe = regexp.MustCompile(`label_values\((\w+),\s?(\w+)\)`)

func main() {
	flag.StringVar(&sourcePath, "src", "./tidb-dashboards", "path to the origin dashboards")
	flag.StringVar(&destinationPath, "dst", "./publish", "path to the publish dashboards")
	flag.BoolVar(&nextGen, "nextgen", false, "process nextgen dashboards")
	flag.Parse()

	fis, err := os.ReadDir(sourcePath)
	if err != nil {
		panic(err)
	}

	for _, fi := range fis {
		if !fi.IsDir() {
			continue
		}

		version := fi.Name()

		if err := filepath.WalkDir(filepath.Join(sourcePath, version), func(fname string, d fs.DirEntry, err error) error {
			if err != nil {
				panic(err)
			}
			if d.IsDir() {
				return nil
			}

			//data := make(dashboardsData)
			bs, err := os.ReadFile(fname)
			if err != nil {
				return err
			}
			if nextGen {
				NewNextGenProcessor(string(bs), fname).process(version)
				return nil
			}

			NewOpClusterProcessor(string(bs), fname).process(version)
			NewOperatorClusterProcessor(string(bs), fname).process(version)
			NewTiDBCloudProcessor(string(bs), fname).process(version)

			return nil
		}); err != nil {
			panic(err)
		}
	}
}

const (
	datasource_name = "Prometheus"
)

type opClusterProcessor struct {
	v    string
	path string
}

func NewOpClusterProcessor(body string, path string) *opClusterProcessor {
	return &opClusterProcessor{
		v:    string(body),
		path: path,
	}
}

func (p *opClusterProcessor) process(version string) {
	v := p.insertUID().replaceDS(datasource_name).replaceTitle().replaceVariables().insertDS()

	v, err := sjson.Set(v, "uid", fmt.Sprintf("%scluster%s", gjson.Get(v, "uid").Str, strings.ReplaceAll(version, ".", "_")))
	if err != nil {
		panic(err)
	}

	targetDir := filepath.Join(destinationPath, "clinic-private-cluster-"+version)
	err = os.MkdirAll(targetDir, 0755)
	if err != nil {
		panic(err)
	}
	fpath := filepath.Join(targetDir, filepath.Base(p.path))
	err = os.WriteFile(fpath, []byte(v), fs.ModePerm)
	if err != nil {
		panic(err)
	}
}

func (p *opClusterProcessor) replaceDS(dsName string) *opClusterProcessor {
	if gjson.Get(p.v, "__inputs").Exists() && gjson.Get(p.v, "__inputs.0.name").Exists() {
		datasource := gjson.Get(p.v, "__inputs.0.name").Str
		v := strings.ReplaceAll(p.v, fmt.Sprintf("${%s}", datasource), dsName)
		v = strings.ReplaceAll(v, "${DS_TEST-CLUSTER}", dsName)
		return &opClusterProcessor{v: v}
	}

	return p
}

func (p *opClusterProcessor) replaceTitle() *opClusterProcessor {

	t := gjson.Get(p.v, "title").Str
	t = strings.ReplaceAll(t, "Test-", "")

	if strings.Contains(t, "Cluster") {
		t = strings.ReplaceAll(t, "Cluster-", "")
		t = fmt.Sprintf("%s-Cluster", t)
	} else if strings.Contains(t, "DM") {
		t = strings.ReplaceAll(t, "Monitor-", "")
	}

	if !strings.HasSuffix(t, "-Clinic") {
		t = fmt.Sprintf("%s-Clinic", t)
	}

	v, err := sjson.Set(p.v, "title", t)
	if err != nil {
		panic(err)
	}

	return &opClusterProcessor{
		v: v,
	}
}

func (p *opClusterProcessor) insertUID() *opClusterProcessor {
	fmt.Println(p.path)
	fmt.Println(filepath.Base(p.path))
	v, err := sjson.Set(p.v, "uid", FnvHash(strings.TrimSuffix(filepath.Base(p.path), ".json")))
	if err != nil {
		panic(err)
	}

	return &opClusterProcessor{
		v:    v,
		path: p.path,
	}
}

func FnvHash(raw string) string {
	hash := fnv.New64()
	if _, err := hash.Write([]byte(raw)); err != nil {
		// impossible path
		panic(err)
	}
	return fmt.Sprintf("%x", hash.Sum64())
}

func (p *opClusterProcessor) replaceVariables() *opClusterProcessor {
	v := p.v
	var err error
	// if title starts with "DM", tidb_cluster label is not used
	if title := gjson.Get(p.v, "title").Str; strings.HasPrefix(title, "DM") {
		gjson.Get(p.v, "templating.list").ForEach(func(key, value gjson.Result) bool {
			if !value.IsObject() {
				return true
			}
			if value.Get("datasource").String() != "Prometheus" || value.Get("type").String() != "query" {
				return true
			}

			if value.Get("name").String() == "k8s_cluster" {
				path := fmt.Sprintf("templating.list.%d.datasource", int(key.Num))
				v, err = sjson.Set(v, path, "Prometheus-fake")
				if err != nil {
					panic(err)
				}
				return true
			}
			query := ""
			path := ""
			if value.Get("query").IsObject() {
				query = value.Get("query.query").String()
				path = fmt.Sprintf("templating.list.%d.query.query", int(key.Num))
			} else {
				query = value.Get("query").String()
				path = fmt.Sprintf("templating.list.%d.query", int(key.Num))
			}
			if query == "" {
				return true
			}
			match := variablesRe.FindStringSubmatch(query)
			if len(match) != 3 {
				return true
			}
			// regexp match and replace
			replacedQuery := fmt.Sprintf(`label_values(%s{k8s_cluster="$k8s_cluster"}, %s)`, match[1], match[2])
			v, err = sjson.Set(v, path, replacedQuery)
			if err != nil {
				panic(err)
			}
			return true
		})
	} else {
		gjson.Get(p.v, "templating.list").ForEach(func(key, value gjson.Result) bool {
			var err error
			if !value.IsObject() {
				return true
			}
			if value.Get("datasource").String() != "Prometheus" || value.Get("type").String() != "query" {
				return true
			}

			if value.Get("name").String() == "k8s_cluster" {
				path := fmt.Sprintf("templating.list.%d.datasource", int(key.Num))
				v, err = sjson.Set(v, path, "Prometheus-fake")
				if err != nil {
					panic(err)
				}
				return true
			}
			query := ""
			path := ""
			if value.Get("query").IsObject() {
				query = value.Get("query.query").String()
				path = fmt.Sprintf("templating.list.%d.query.query", int(key.Num))
			} else {
				query = value.Get("query").String()
				path = fmt.Sprintf("templating.list.%d.query", int(key.Num))
			}
			if query == "" {
				return true
			}
			match := variablesRe.FindStringSubmatch(query)
			if len(match) != 3 {
				return true
			}
			// regexp match and replace
			replacedQuery := fmt.Sprintf(`label_values(%s{tidb_cluster="$tidb_cluster"}, %s)`, match[1], match[2])
			v, err = sjson.Set(v, path, replacedQuery)
			if err != nil {
				panic(err)
			}
			return true
		})
	}
	return &opClusterProcessor{v: v}
}

func (p *opClusterProcessor) insertDS() string {

	variables := []string{
		`{
        "allValue": null,
        "datasource": "Prometheus-fake",
        "definition": "label_values({job=\"pd\"}, tidb_cluster)",
        "description": null,
        "error": null,
        "hide": 0,
        "includeAll": false,
        "label": "tidb_cluster",
        "multi": false,
        "name": "tidb_cluster",
        "options": [],
        "query": {
          "query": "label_values({job=\"pd\"}, tidb_cluster)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      }`,
		`{
        "description": null,
        "error": null,
        "hide": 2,
        "label": null,
        "name": "org",
        "query": "",
        "skipUrlSync": false,
        "type": "constant"
      }`,
		`{
        "description": null,
        "error": null,
        "hide": 2,
        "label": null,
        "name": "tidb_version",
        "query": "",
        "skipUrlSync": false,
        "type": "constant"
      }`,
		`{
        "description": null,
        "error": null,
        "hide": 2,
        "label": null,
        "name": "org_type",
        "query": "",
        "skipUrlSync": false,
        "type": "constant"
      }`,
	}

	v := p.v

	for _, variable := range variables {
		v = insertVariable(v, variable)
	}
	//var m map[string]json.RawMessage
	//err := json.Unmarshal([]byte(fake), &m)
	//if err != nil {
	//	panic(err)
	//}
	//
	//v, err := sjson.Set(p.v, fmt.Sprintf("templating.list.%d", len(list)), m)
	//if err != nil {
	//	panic(err)
	//}
	return v
}

func insertVariable(v string, variable string) string {
	list := gjson.Get(v, "templating.list").Array()

	var m map[string]json.RawMessage
	err := json.Unmarshal([]byte(variable), &m)
	if err != nil {
		panic(err)
	}

	v, err = sjson.Set(v, fmt.Sprintf("templating.list.%d", len(list)), m)
	if err != nil {
		panic(err)
	}
	return v
}

type tidbCloudClusterProcessor struct {
	*operatorClusterProcessor
	//fileName string
	//path string
}

func NewTiDBCloudProcessor(body string, path string) *tidbCloudClusterProcessor {
	return &tidbCloudClusterProcessor{
		operatorClusterProcessor: &operatorClusterProcessor{
			opClusterProcessor: &opClusterProcessor{
				v:    string(body),
				path: path,
			},
		},
	}
}

func (cp *tidbCloudClusterProcessor) process(version string) {
	dsName := datasource_name
	title := gjson.Get(cp.v, "title").Str
	if strings.HasPrefix(title, "DM") {
		dsName = "project-metrics"
	}
	ov := cp.insertUID().replaceDS(dsName).replaceTitle().replaceVariables().insertDS()

	v, err := sjson.Set(ov, "uid", fmt.Sprintf("%stidbcloud%s", gjson.Get(ov, "uid").Str, strings.ReplaceAll(version, ".", "_")))
	if err != nil {
		panic(err)
	}

	if strings.HasPrefix(title, "DM") {
		// DM metrics do not have cluster_id label
		v = strings.ReplaceAll(v, `tidb_cluster=~\"$tidb_cluster\", `, "")
	}

	if shouldApplyRecordingRule(destinationPath) {
		v = AddPanelExprUseRecordingRule(cp.path, v)
	}

	// v, err := sjson.Set(newValue, "title", strings.ReplaceAll(gjson.Get(newValue, "title").Str, "Clinic", "Clinic-TiDB-Cloud"))
	// if err != nil {
	// 	panic(err)
	// }
	// for case like:
	// "query": "label_values(pd_cluster_status{k8s_cluster=\"$k8s_cluster\", tidb_cluster=\"$tidb_cluster\"}, instance)"
	// sum(rate(tidb_server_handle_query_duration_seconds_sum{tidb_cluster="$tidb_cluster", sql_type!="internal"}[1m]))
	v = strings.ReplaceAll(v, `tidb_cluster=\"$tidb_cluster\"`, `tidb_cluster_id=\"$tidb_cluster\"`)
	v = strings.ReplaceAll(v, `tidb_cluster=~\"$tidb_cluster.*\"`, `tidb_cluster_id=\"$tidb_cluster\"`)

	targetDir := filepath.Join(destinationPath, "clinic-private-tidbcloud-"+version)
	err = os.MkdirAll(targetDir, 0755)
	if err != nil {
		panic(err)
	}
	fpath := filepath.Join(targetDir, filepath.Base(cp.path))
	if err := os.WriteFile(fpath, []byte(v), fs.ModePerm); err != nil {
		panic(err)
	}

	cp.replaceJob(fpath)
}

func shouldApplyRecordingRule(dst string) bool {
	return strings.Contains(filepath.Clean(dst), "internal-dashboards")
}

type nextGenProcessor struct {
	*operatorClusterProcessor
	//fileName string
	//path string
}

func NewNextGenProcessor(body string, path string) *nextGenProcessor {
	return &nextGenProcessor{
		operatorClusterProcessor: &operatorClusterProcessor{
			opClusterProcessor: &opClusterProcessor{
				v:    string(body),
				path: path,
			},
		},
	}
}

func (cp *nextGenProcessor) process(version string) {
	dsName := datasource_name
	title := gjson.Get(cp.v, "title").Str
	if strings.HasPrefix(title, "DM") {
		dsName = "project-metrics"
	}
	ov := cp.insertUID().replaceDS(dsName).replaceTitle().replaceVariables().insertDS()

	v, err := sjson.Set(ov, "uid", fmt.Sprintf("%snextgen%s", gjson.Get(ov, "uid").Str, strings.ReplaceAll(version, ".", "_")))
	if err != nil {
		panic(err)
	}

	if strings.HasPrefix(title, "DM") {
		// DM metrics do not have cluster_id label
		v = strings.ReplaceAll(v, `tidb_cluster=~\"$tidb_cluster\", `, "")
	}
	v = strings.ReplaceAll(v, `tidb_cluster=\"$tidb_cluster\"`, `tidb_cluster_id=\"$tidb_cluster\"`)
	v = strings.ReplaceAll(v, `tidb_cluster=~\"$tidb_cluster.*\"`, `tidb_cluster_id=\"$tidb_cluster\"`)

	targetDir := filepath.Join(destinationPath, "clinic-private-nextgen-"+version)
	err = os.MkdirAll(targetDir, 0755)
	if err != nil {
		panic(err)
	}
	fpath := filepath.Join(targetDir, filepath.Base(cp.path))
	if err := os.WriteFile(fpath, []byte(v), fs.ModePerm); err != nil {
		panic(err)
	}

	cp.replaceJob(fpath)
}

type operatorClusterProcessor struct {
	*opClusterProcessor
}

func NewOperatorClusterProcessor(body string, path string) *operatorClusterProcessor {
	return &operatorClusterProcessor{
		opClusterProcessor: &opClusterProcessor{
			v:    string(body),
			path: path,
		},
	}
}

func (cp *operatorClusterProcessor) process(version string) {

	ov := cp.insertUID().replaceDS(datasource_name).replaceTitle().replaceVariables().insertDS()
	v, err := sjson.Set(ov, "uid", fmt.Sprintf("%soperator%s", gjson.Get(ov, "uid").Str, strings.ReplaceAll(version, ".", "_")))
	if err != nil {
		panic(err)
	}

	// v, err := sjson.Set(newValue, "title", strings.ReplaceAll(gjson.Get(newValue, "title").Str, "Clinic", "Clinic-Operator"))
	// if err != nil {
	// 	panic(err)
	// }

	targetDir := filepath.Join(destinationPath, "clinic-private-operator-"+version)
	err = os.MkdirAll(targetDir, 0755)
	if err != nil {
		panic(err)
	}
	fpath := filepath.Join(targetDir, filepath.Base(cp.path))
	if err := os.WriteFile(fpath, []byte(v), fs.ModePerm); err != nil {
		panic(err)
	}

	cp.replaceJob(fpath)
}

// use sed to do some reg related inplace replacement.
func (cp *operatorClusterProcessor) replaceJob(path string) {
	commands := []*exec.Cmd{
		exec.Command("sed", "-i", "s%job=\\\\\\\"tiflash\\\\\\\"%component=\\\\\"tiflash\\\\\"%g", path),
		exec.Command("sed", "-i", "s%job=\\\\\\\"tikv-importer\\\\\\\"%component=\\\\\"importer\\\\\"%g", path),
		exec.Command("sed", "-i", "s%job=\\\\\\\"lightning\\\\\\\"%component=\\\\\"tidb-lightning\\\\\"%g", path),
		exec.Command("sed", "-i", "s%job=%component=%g", path),
		exec.Command("sed", "-i", "s%{{job}}%{{component}}%g", path),
		exec.Command("sed", "-i", "-e", "s%\\(by\\s(\\)job\\(,.*)\\)%\\1component\\2%g", "-e", "s%\\(by\\s(.*\\),job,\\(.*)\\)%\\1,component,\\2%g", "-e", "s%\\(by\\s(.*,\\)job)%\\1component)%g", "-e", "s%\\(by\\s(\\)job)%\\1component)%g", path),
		exec.Command("sed", "-i", "-e", "s%tidb_tikvclient_cop_duration_seconds_bucket{%tidb_tikvclient_request_seconds_bucket{store!=\\\\\"0\\\\\", scope=\\\\\"false\\\\\", type=\\\\\"Cop\\\\\", %g", path),
		// sum(rate(tikv_thread_cpu_seconds_total{cluster_id=~".*$cluster_id", instance=~"$instance"}[1m])) by (instance) ->
		// sum(rate(tikv_thread_cpu_seconds_total{cluster_id=~".*$cluster_id", instance=~"$instance"}[2m])) by (instance)
		// irate(process_cpu_seconds_total{k8s_cluster="$k8s_cluster", cluster_id=~".*$tidb_cluster",component=~".*pd.*"}[30s]) ->
		// irate(process_cpu_seconds_total{k8s_cluster="$k8s_cluster", cluster_id=~".*$tidb_cluster",component=~".*pd.*"}[2m])
		exec.Command("sed", "-i", "-E", "s%((rate|irate|delta)\\(\\w*\\{[^}]*\\})(\\[(1m|30s)\\])%\\1[2m]%g", path),
	}

	for _, cmd := range commands {
		err := cmd.Run()
		if err != nil {
			fmt.Printf("command execute failed, command=%v, err=%v\n", cmd.String(), err)
			panic(err)
		}
	}
}

/*
func getDirAndFileName(path string) (string, string) {
	index := strings.LastIndex(path, "/")
	dir := path[:index]
	if filepath.Base(dir) == "default" {
		index := strings.LastIndex(dir, "/")
		dir = dir[:index]
	}
	return dir, path[index+1:]
}
*/
