package client

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/pingcap/log"
	"github.com/tidbcloud/metrics-server/central/metrics"
	"github.com/tidbcloud/metrics-server/internal/datasource"
	"go.uber.org/zap"
)

var (
	QueryRequestStatusEmpty   = "empty"
	QueryRequestStatusError   = "error"
	QueryRequestStatusSuccess = "success"
)

type LabelSet struct {
	MetricName          string `json:"__name__,omitempty"`
	Component           string `json:"component,omitempty"`
	Instance            string `json:"instance,omitempty"`
	Job                 string `json:"job,omitempty"`
	KubernetesNamespace string `json:"kubernetes_namespace,omitempty"`
	Name                string `json:"name,omitempty"`
	Replica             string `json:"replica,omitempty"`
	SeedAPI             string `json:"seed_api,omitempty"`
	SeedProvider        string `json:"seed_provider,omitempty"`
	SeedRegion          string `json:"seed_region,omitempty"`
	ShootInfra          string `json:"shoot_infra,omitempty"`
	ShootName           string `json:"shoot_name,omitempty"`
	Tenant              string `json:"tenant,omitempty"`
	TenantID            string `json:"tenant_id,omitempty"`
	TidbCluster         string `json:"tidb_cluster,omitempty"`
	Type                string `json:"type,omitempty"`
	SqlType             string `json:"sql_type,omitempty"`
	Le                  string `json:"le,omitempty"`
	Result              string `json:"result,omitempty"`
	InTxn               string `json:"in_txn,omitempty"`
	TxnMode             string `json:"txn_mode,omitempty"`
	Changefeed          string `json:"changefeed,omitempty"`
	ResourceGroup       string `json:"resource_group,omitempty"`
	Device              string `json:"device"`
}

type Result struct {
	LabelSet `json:"metric"`
	Value    []interface{} `json:"value"`
}

type Response struct {
	Status string `json:"status"`
	Data   struct {
		ResultType string   `json:"resultType"`
		Result     []Result `json:"result"`
	} `json:"data"`
}

func DoInstantQuery(ctx context.Context, q string, metric string) (resp *Response, err error) {
	addr := datasource.MetricDataAddress{}
	defer func() {
		clusterId := strconv.FormatUint(addr.TidbClusterInfo.ClusterID.Uint64(), 10)
		logger := log.With(zap.String("tidb_cluster_vendor", addr.TidbClusterInfo.Vendor),
			zap.String("tidb_cluster_region", addr.TidbClusterInfo.Region),
			zap.Uint64("tidb_cluster_tenant_id", addr.TidbClusterInfo.TenantID.Uint64()),
			zap.Uint64("tidb_cluster_project_id", addr.TidbClusterInfo.ProjectID.Uint64()),
			zap.String("tidb_cluster_id", clusterId),
			zap.String("IntegrationType", addr.IntegrationType),
			zap.String("QueryUrl", addr.QueryUrl),
			zap.String("PromQL", q),
			zap.String("ExportedMetric", metric))
		if err != nil || resp.Status != "success" {
			// use metric instead of promql as label to avoid high cardinality.
			metrics.O11yServerQueryRequest.WithLabelValues(addr.QueryUrl, clusterId, metric, QueryRequestStatusError).Inc()
			logger.Sugar().Errorf("doInstantQuery fail, err %v, result: %+v", zap.Error(err), resp)
		} else if len(resp.Data.Result) == 0 {
			metrics.O11yServerQueryRequest.WithLabelValues(addr.QueryUrl, clusterId, metric, QueryRequestStatusEmpty).Inc()
		} else {
			// FIXME: we don't know how to cleanup those metrics when the cluster is deleted.
			metrics.O11yServerQueryRequest.WithLabelValues(addr.QueryUrl, clusterId, metric, QueryRequestStatusSuccess).Inc()
			logger.Debug("doInstantQuery success", zap.Any("result", resp.Data.Result))
		}
	}()

	addr, err = datasource.GetMetricDataAddressFromContext(ctx)
	if err != nil {
		return nil, err
	}

	queryUrl := addr.QueryUrl
	reqCnt := 3
	for i := 0; i < reqCnt; i++ {
		result, canRetry, err := queryMetricData(ctx, queryUrl, q)
		if err == nil {
			return result, nil
		}

		if i == reqCnt-1 {
			return nil, err
		}

		if canRetry {
			time.Sleep(100 * time.Millisecond * time.Duration(i+1))
			continue
		}

		return nil, err
	}

	return nil, errors.New("unknown error for instant query")
}

func queryMetricData(ctx context.Context, queryUrl, urlParams string) (thanosResp *Response, canRetry bool, err error) {
	startTime := time.Now()
	path := "/api/v1/query"
	method := "GET"
	defer func() {
		duration := float64(time.Since(startTime)) / float64(time.Millisecond)
		metrics.O11yServerRequestDuration.WithLabelValues(queryUrl, path, method).Observe(duration)
		if err != nil {
			metrics.O11yServerRequestErrCnt.WithLabelValues(queryUrl, path, method).Inc()
		}
	}()

	urlString := fmt.Sprintf("%s%s?query=%s", queryUrl, path, url.QueryEscape(urlParams))
	req, err := http.NewRequestWithContext(ctx, method, urlString, nil)
	if err != nil {
		return nil, false, err
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, true, err
	}
	defer resp.Body.Close() //nolint

	if resp.StatusCode != http.StatusOK {
		body, _ := ioutil.ReadAll(resp.Body)
		return nil, true, fmt.Errorf("http response status %s from metric data server, body: %s", resp.Status, body)
	}

	thanosNodeMemoryRespResult := Response{}
	if err := json.NewDecoder(resp.Body).Decode(&thanosNodeMemoryRespResult); err != nil {
		return nil, false, err
	}
	return &thanosNodeMemoryRespResult, false, nil
}
