package metrics

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/common/model"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"go.uber.org/zap"
)

// External prometheus url example: "www.ds.us-west-2.aws.observability.tidbcloud.com"
// details:
// [www.ds] represents the prod data plane.
// [us-west-2] represents the region name of a cluster.
// [aws] represents the provider, aws or gcp.

const (
	DataPlanePrefix         = "www.ds"
	DataPlaneSuffix         = "observability.tidbcloud.com"
	ObservabilityAuthURL    = "https://tidb-soc2.us.auth0.com/oauth/token"
	ObservabilityClusterURL = "https://www.gs.us-west-2.aws.observability.tidbcloud.com/api/v1/clusters"
)

const (
	devEnv     string = "dev-tidbcloud"
	stagingEnv string = "staging-tidbcloud"
	prodEnv    string = "tidbcloud"
)

var metricsGaugeVecMap = make(map[string]*prometheus.GaugeVec)

type Cluster struct {
	ApplicationID string   `json:"applicationID"`
	BizType       string   `json:"bizType"`
	ID            string   `json:"id"`
	Note          string   `json:"note"`
	ProjectID     string   `json:"projectID"`
	Region        string   `json:"region"`
	ShareMode     string   `json:"shareMode"`
	Tags          []string `json:"tags"`
	TenantID      string   `json:"tenantID"`
	Vendor        string   `json:"vendor"`
	Version       string   `json:"version"`
}

type ClusterResponse struct {
	Code int `json:"code"`
	Data struct {
		Clusters           []Cluster `json:"clusters"`
		ContinueToken      string    `json:"continueToken"`
		RemainingItemCount int       `json:"remainingItemCount"`
	} `json:"data"`
	Message string `json:"message"`
}

type MetricsResponse struct {
	Status string `json:"status"`
	Data   struct {
		ResultType string       `json:"resultType"`
		Result     model.Matrix `json:"result"`
	} `json:"data"`
}

// GetAuthToken will send a request to observability server to get the token for next requests.
func GetAuthToken(cli *http.Client, clientId string, clientSecret string) (string, error) {
	data := map[string]string{
		"client_id":     clientId,
		"client_secret": clientSecret,
		"grant_type":    "client_credentials",
		"audience":      "https://tidb-soc2.us.auth0.com/api/v2/",
	}
	dataBytes, err := json.Marshal(data)
	if err != nil {
		return "", errors.Trace(err)
	}
	resp, err := cli.Post(ObservabilityAuthURL, "application/json", bytes.NewBuffer(dataBytes))
	if err != nil {
		return "", errors.Trace(err)
	}
	defer func() {
		_ = resp.Body.Close()
	}()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", errors.Trace(err)
	}
	var responseData map[string]interface{}
	err = json.Unmarshal(body, &responseData)
	if err != nil {
		return "", errors.Trace(err)
	}
	/* example response from documents
	{
	    "access_token": "bGciOiJSUzI1NiIsInR5ssssssssssssssssssssssssss",
	    "scope": "read:client_grants",
	    "expires_in": 86400,
	    "token_type": "Bearer"
	}
	*/
	accessToken, ok := responseData["access_token"]
	if ok {
		return accessToken.(string), nil
	}
	return "", errors.New("cannot find access token in response")
}

// GetPrometheusURLForCluster represents the cluster level prometheus url.
func GetPrometheusURLForCluster(cli *http.Client, cluster *core.Cluster, bizType, token string) (string, error) {
	parameters := url.Values{}
	parameters.Add("vendor", cluster.CloudProviderName)
	parameters.Add("region", cluster.RegionName)
	parameters.Add("bizType", bizType)
	parameters.Add("applicationID", strconv.FormatUint(cluster.Id, 10))
	oURL := fmt.Sprintf("%s?%s", ObservabilityClusterURL, parameters.Encode())
	req, err := http.NewRequest("GET", oURL, nil)
	if err != nil {
		return "", errors.Trace(err)
	}
	req.Header.Add("Authorization", "Bearer "+token)
	resp, err := cli.Do(req)
	if err != nil {
		return "", errors.Trace(err)
	}
	defer func() {
		_ = resp.Body.Close()
	}()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", errors.Trace(err)
	}
	var responseData ClusterResponse

	err = json.Unmarshal(body, &responseData)
	if err != nil {
		return "", errors.Trace(err)
	}
	var o11yClusterID string
	if len(responseData.Data.Clusters) > 0 {
		o11yClusterID = responseData.Data.Clusters[0].ID
	}
	if len(o11yClusterID) == 0 {
		return "", errors.New("cannot find o11y cluster")
	}
	return fmt.Sprintf("https://%s.%s.%s.%s/internal/metrics/%s",
		DataPlanePrefix, cluster.RegionName, cluster.CloudProviderName, DataPlaneSuffix, o11yClusterID), nil
}

// GetPrometheusTimeRange represents the time range of one request.
func GetPrometheusTimeRange() (string, string) {
	endTime := time.Now().UTC()
	startTime := endTime.Add(-15 * time.Minute)
	return startTime.Format("2006-01-02T15:04:05Z"), endTime.Format("2006-01-02T15:04:05Z")
}

// CollectMetrics the metric from a dataplane cluster to the destination prometheus cluster.
func CollectMetrics(cli *http.Client, o11yPromURL string, metric string, startTime string, endTime string) error {
	params := url.Values{}
	params.Add("query", metric)
	params.Add("start", startTime)
	params.Add("end", endTime)
	params.Add("step", "15m")

	resp, err := cli.Get(fmt.Sprintf("%s/api/v1/query_range?%s", o11yPromURL, params.Encode()))
	if err != nil {
		return errors.Trace(err)
	}
	defer func() {
		_ = resp.Body.Close()
	}()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return errors.Trace(err)
	}
	var metricResp MetricsResponse
	err = json.Unmarshal(body, &metricResp)
	if err != nil {
		return errors.Trace(err)
	}
	if metricResp.Status != "success" {
		return errors.New("failed to get metrics from cluster")
	}
	buildMetricsGaugeVec(metric, metricResp.Data.Result)
	return nil
}

// we need transform json to protobuf to write to promtheus
func buildMetricsGaugeVec(metricName string, data model.Matrix) {
	if len(data) == 0 {
		return
	}
	// already check the data not empty
	labelNames := make([]string, 0, len(data[0].Metric))
	for labelName := range data[0].Metric {
		if strings.HasPrefix(string(labelName), "_") {
			// we should skip invalid label. e.g. __name__
			continue
		}
		labelNames = append(labelNames, string(labelName))
	}

	// sort label names for insert
	sort.Strings(labelNames)

	gaugeVec, ok := metricsGaugeVecMap[metricName]
	if !ok {
		gaugeVec = prometheus.NewGaugeVec(
			prometheus.GaugeOpts{
				Namespace: "dataflow",
				Subsystem: "dr",
				Name:      metricName,
			}, labelNames)
		// first register the metrics
		prometheus.MustRegister(gaugeVec)
		metricsGaugeVecMap[metricName] = gaugeVec
	}

	// collect for gc
	gaugeVec.Reset()
	for _, stream := range data {
		labelValues := make([]string, 0, len(data[0].Metric))
		for _, labelName := range labelNames {
			if strings.HasPrefix(labelName, "_") {
				// we should skip invalid label. e.g. __name__
				continue
			}
			labelValue := stream.Metric[model.LabelName(labelName)]
			labelValues = append(labelValues, string(labelValue))
		}
		value := stream.Values[len(stream.Values)-1]
		gaugeVec.WithLabelValues(labelValues...).Set(float64(value.Value))
	}
}

// GetBizType can be a filter for different environment clusters.
// It's a proper noun for o11y to distinguish different environments.
//
//	for prod: "tidbcloud"
//	for staging: "staging-tidbcloud"
//	for dev: "dev-tidbcloud"
func GetBizType(env string) string {
	switch env {
	case "nightly":
		return devEnv
	case "staging":
		return stagingEnv
	case "prod":
		return prodEnv
	default:
		log.L().Warn("unable to parse env", zap.String("env", env))
		return ""
	}
}
