package collectbaseline

import (
	"context"
	"database/sql"
	"fmt"
	"math"
	"net/url"
	"strings"
	"time"

	. "github.com/pingcap/endless/pkg/matcher"
	. "github.com/pingcap/endless/pkg/util"
	"github.com/pingcap/endless/pkg/workloadnode"
	"github.com/pingcap/endless/testcase/oltp/oltppkg"
	"github.com/pingcap/log"
	"github.com/pingcap/test-infra/sdk/resource"
	"github.com/prometheus/client_golang/api"
	v1 "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/prometheus/common/model"
	"golang.org/x/sync/errgroup"
)

func OLTP_COLLECT_BASILINE(
	ctx context.Context,
	tc resource.TiDBCluster,
	caseCmdParms oltppkg.CaseCmdParms,
	workloadRunParms oltppkg.WorkloadRunParms,
	br resource.BR,
	s3Parms oltppkg.S3Parms,
) {
	runTime := 10 * time.Hour
	ctxAll, cancel := context.WithTimeout(ctx, runTime)
	defer cancel()
	g, ctxg := errgroup.WithContext(ctxAll)

	g.Go(WithGinkgoRecover(func() {
		workloadRunParms.Pause(ctxg, runTime)
	}))

	workloadList := []string{"sysbench_oltp_insert", "sysbench_oltp_write_only",
		"sysbench_oltp_delete", "sysbench_oltp_update_index", "sysbench_oltp_update_non_index",
		"sysbench_oltp_read_write", "sysbench_oltp_read_only", "sysbench_oltp_point_select",
		"sysbench_select_random_points", "sysbench_select_random_ranges", "tpcc"}
	versionMap, gitHashMap := GetGitVersion(tc)
	for _, workload := range workloadList {
		for i, randType := range []string{"uniform", "gaussian", "special", "pareto"} {
			if !strings.HasPrefix(workload, "sysbench") {
				randType = ""
				if i > 0 {
					log.Info("Only sysbench need run all randType")
					break
				}
			}
			startTime := time.Now()
			runcmd := oltppkg.BuildWorkloadRuncmd(ctx, tc, workload, workloadRunParms.WorkloadNode, &caseCmdParms, randType)
			cmd := Try(oltppkg.ExecWorkloadCmd(ctx, workloadRunParms.WorkloadNode, runcmd, false, nil)).(*workloadnode.Cmd)
			oltppkg.WaitFor(ctx, "running 20min", 10*time.Minute)
			cmd.Stop()
			endTime := time.Now()
			workloadRecord := OltpWorkloadBenchMark{
				Workload:       workload,
				RandomType:     randType,
				TidbVersion:    versionMap["tidb"],
				TidbGitVersion: gitHashMap["tidb"],
				TikvVersion:    versionMap["tikv"],
				TikvGitVersion: gitHashMap["tikv"],
				PdVersion:      versionMap["pd"],
				PdGitVersion:   gitHashMap["pd"],
				StartTime:      startTime,
				EndTime:        endTime,
			}
			getBenchResult(Try(tc.ServiceURL(resource.Prometheus)).(*url.URL), 180, time.Now().Add(-2*time.Minute), &workloadRecord)
			log.Info(fmt.Sprintf("will write to %v", workloadRecord))
			Try(SaveToDb(workloadRecord))
		}
	}
}

type OltpWorkloadBenchMark struct {
	Workload           string
	PdVersion          string
	PdGitVersion       string
	TikvVersion        string
	TikvGitVersion     string
	TidbVersion        string
	TidbGitVersion     string
	RandomType         string
	AvgQps             float64
	AvgTps             float64
	AvgP90             float64
	AvgP99             float64
	AvgLatency         float64
	MaxLatency         float64
	StartTime          time.Time
	EndTime            time.Time
	Tikv_cpu_avg       float64
	Tikv_cpu_delta_avg float64
}

func GetGitVersion(tc resource.TiDBCluster) (map[string]string, map[string]string) {
	commitQuery := `select distinct type,version,git_hash from information_schema.cluster_info`
	db := Try(tc.GetDB("")).(*sql.DB)
	rows, err := db.Query(commitQuery)
	if err != nil {
		panic(err)
	}
	versionMap := map[string]string{"tidb": "", "tikv": "", "pd": "", "tiflash": ""}
	gitHashMap := map[string]string{"tidb": "", "tikv": "", "pd": "", "tiflash": ""}

	for rows.Next() {
		type row struct {
			serverType string
			version    string
			gitHash    string
		}
		var r row
		err = rows.Scan(&r.serverType, &r.version, &r.gitHash)
		if err != nil {
			panic(err)
		}
		versionMap[r.serverType] = r.version
		gitHashMap[r.serverType] = r.gitHash
	}
	return versionMap, gitHashMap
}
func SaveToDb(workloadRecord OltpWorkloadBenchMark) error {
	dbHost := "172.16.4.36"
	dbPort := 3306
	userName := "oltp_user"
	passWord := "We7nhQj8AUlRg5Q"
	dbName := "oltpbenchtest"
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", userName, passWord, dbHost, dbPort, dbName)
	db, err := sql.Open("mysql", dsn)

	if err != nil {
		return err
	}
	defer db.Close()
	insertTemp := `insert into oltp_workload_benchmark(workload,rand_type,tidbversion,tidbgithash,tikvversion,tikvgithash,pdversion,pdgithash,avgQps,avgTps,avgP90,avgP99,avgLatency,maxLatency,starttime,endtime,tikv_cpu_avg,tikv_cpu_delta_avg) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
	_, err = db.Exec(insertTemp, workloadRecord.Workload, workloadRecord.RandomType,
		workloadRecord.TidbVersion, workloadRecord.TidbGitVersion,
		workloadRecord.TikvVersion, workloadRecord.TikvGitVersion,
		workloadRecord.PdVersion, workloadRecord.PdGitVersion,
		workloadRecord.AvgQps, workloadRecord.AvgTps, workloadRecord.AvgP90, workloadRecord.AvgP99,
		workloadRecord.AvgLatency, workloadRecord.MaxLatency,
		workloadRecord.StartTime.Format(time.RFC3339),
		workloadRecord.EndTime.Format(time.RFC3339),
		workloadRecord.Tikv_cpu_avg,
		workloadRecord.Tikv_cpu_delta_avg,
	)
	if err != nil {
		println(err.Error())
		return err
	}
	return nil
}

func getBenchResult(promURL *url.URL, seconds int64, endTime time.Time, workloadRecord *OltpWorkloadBenchMark) {
	workloadRecord.AvgQps = getMetric(promURL, fmt.Sprintf(`avg_over_time(sum(rate(tidb_server_query_total[1m])) [%ds:])`, seconds), endTime)
	workloadRecord.AvgTps = getMetric(promURL, fmt.Sprintf(`avg_over_time(sum(rate(tidb_session_transaction_duration_seconds_count[1m])) [%ds:])`, seconds), endTime)
	workloadRecord.AvgP90 = getMetric(promURL, fmt.Sprintf(`1000*avg_over_time(histogram_quantile(0.90, sum(rate(tidb_server_handle_query_duration_seconds_bucket[1m])) by (le))[%ds:])`, seconds), endTime)
	workloadRecord.AvgP99 = getMetric(promURL, fmt.Sprintf(`1000*avg_over_time(histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket[1m])) by (le))[%ds:])`, seconds), endTime)
	//avg latency, max latency
	workloadRecord.AvgLatency = getMetric(promURL, fmt.Sprintf(`1000*avg_over_time((sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[1m])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[1m])))[%ds:])`, seconds), endTime)
	workloadRecord.MaxLatency = getMetric(promURL, fmt.Sprintf(`1000*max_over_time(histogram_quantile(1, sum(rate(tidb_server_handle_query_duration_seconds_bucket[1m])) by (le))[%ds:])`, seconds), endTime)
	// tidb_cpu_avg := getMetric(promURL, fmt.Sprintf(`avg_over_time(avg(rate(process_cpu_seconds_total{job="tidb"}[1m]))[%ds:])`, seconds), endTime)
	// tidb_cpu_delta_avg := getMetric(promURL, fmt.Sprintf(`avg_over_time((max(rate(process_cpu_seconds_total{job="tidb"}[1m])) - min(rate(process_cpu_seconds_total{job="tidb"}[1m])))[%ds:])`, seconds), endTime)
	workloadRecord.Tikv_cpu_avg = getMetric(promURL, fmt.Sprintf(`avg_over_time(avg(rate(process_cpu_seconds_total{job=~".*tikv"}[1m]))[%ds:])`, seconds), endTime)
	workloadRecord.Tikv_cpu_delta_avg = getMetric(promURL, fmt.Sprintf(`avg_over_time((max(rate(process_cpu_seconds_total{job=~".*tikv"}[1m])) - min(rate(process_cpu_seconds_total{job=~".*tikv"}[1m])))[%ds:])`, seconds), endTime)
}

func getMetric(promURL *url.URL, query string, ts time.Time) float64 {
	//promURL := Try(tc.ServiceURL(resource.Prometheus)).(*url.URL)
	prometheusAddr := promURL.String()
	client, err := api.NewClient(api.Config{
		Address: prometheusAddr,
	})
	if err != nil {
		panic(err)
	}

	httpAPI := v1.NewAPI(client)

	// Record the current value of the query results.
	val, _, err := httpAPI.Query(context.Background(), query, ts)
	if err != nil {
		panic(err)
	}
	//log.Info("Record the query and value",
	//ap.String("query", query),
	//ap.String("value", format.Object(val, 0)),
	//ap.Time("ts", ts))
	valMatrix := val.(model.Vector)
	// fix the Error 1690: constant NaN overflows double, the value from prom exepression is not a number
	// http://172.16.4.180:31714/artifacts/testground/plan-exec-600556-st7xs/plan-exec-600556-st7xs-428117755/main-logs
	value := float64(valMatrix[0].Value)
	if math.IsNaN(value) {
		return 0
	} else {
		return value
	}
}
