/******************************************************************************
 * Copyright (c)  2022 PingCAP                                                *
 * Licensed under the Apache License, Version 2.0 (the "License");            *
 * you may not use this file except in compliance with the License.           *
 * You may obtain a copy of the License at                                    *
 *                                                                            *
 * http://www.apache.org/licenses/LICENSE-2.0                                 *
 *                                                                            *
 * Unless required by applicable law or agreed to in writing, software        *
 * distributed under the License is distributed on an "AS IS" BASIS,          *
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
 * See the License for the specific language governing permissions and        *
 * limitations under the License.                                             *
 ******************************************************************************/

/*******************************************************************************
 * @File: historical.go
 * @Description:
 * @Author: jiangxunyu@pingcap.com
 * @Version: 1.0.0
 * @Date: 2022/7/8 16:27
*******************************************************************************/

package metrics

import (
	"fmt"
	"net/http"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/observability/pkg/common/structs"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/observability/pkg/common/constants"
)

var once sync.Once
var metrics *Metrics

type Metrics struct {
	// process boot metrics
	BootTimeGaugeMetric *prometheus.GaugeVec

	// api metrics
	APIRequestsCounterMetric       *prometheus.CounterVec
	RequestDurationHistogramMetric *prometheus.HistogramVec
	RequestSizeHistogramMetric     *prometheus.HistogramVec
	ResponseSizeHistogramMetric    *prometheus.HistogramVec

	// error handler total metrics
	ErrorHandlerTotalCounterMetric *prometheus.CounterVec

	// global server metrics
	GlobalRoutingRegionalTotalGaugeMetric *prometheus.GaugeVec
	GlobalRoutingRegionalInfoGaugeMetric  *prometheus.GaugeVec

	// regional server metrics
	RegionalControllerRunsTotalCounterMetric *prometheus.CounterVec

	// ft dial test metrics
	ErrorDialTestMetricsInfoCounterMetric    *prometheus.CounterVec
	ErrorDialTestRegionalServerCounterMetric *prometheus.CounterVec
	DialTestLogsInfoCounterMetric            *prometheus.CounterVec

	// vendor client api metrics
	AWSAPIRequestCounterMetric      *prometheus.CounterVec
	GCPAPIRequestCounterMetric      *prometheus.CounterVec
	AliCloudAPIRequestCounterMetric *prometheus.CounterVec
}

func GetMetrics() *Metrics {
	if metrics == nil {
		panic("GetMetrics must after run SetupMetrics")
	}
	return metrics
}

func SetupMetrics(register ...prometheus.Registerer) {
	once.Do(func() {
		if len(register) > 0 {
			metrics = &Metrics{
				BootTimeGaugeMetric: RegisterNewGaugeVecWithRegister(register[0], BootTimeGaugeMetricDef),

				APIRequestsCounterMetric:       RegisterNewCounterVecWithRegister(register[0], APIRequestsCounterMetricDef),
				RequestDurationHistogramMetric: RegisterNewHistogramVecWithRegister(register[0], RequestDurationHistogramMetricDef),
				RequestSizeHistogramMetric:     RegisterNewHistogramVecWithRegister(register[0], RequestSizeHistogramMetricDef),
				ResponseSizeHistogramMetric:    RegisterNewHistogramVecWithRegister(register[0], ResponseSizeHistogramMetricDef),

				ErrorHandlerTotalCounterMetric: RegisterNewCounterVecWithRegister(register[0], BizErrorTotalCounterMetricDef),

				GlobalRoutingRegionalTotalGaugeMetric: RegisterNewGaugeVecWithRegister(register[0], GlobalRoutingRegionalTotalGaugeMetricDef),
				GlobalRoutingRegionalInfoGaugeMetric:  RegisterNewGaugeVecWithRegister(register[0], GlobalRoutingRegionalInfoGaugeMetricDef),

				RegionalControllerRunsTotalCounterMetric: RegisterNewCounterVecWithRegister(register[0], RegionalControllerRunsTotalCounterMetricDef),

				ErrorDialTestMetricsInfoCounterMetric:    RegisterNewCounterVecWithRegister(register[0], ErrorDialTestMetricsInfoCounterMetricDef),
				ErrorDialTestRegionalServerCounterMetric: RegisterNewCounterVecWithRegister(register[0], ErrorDialTestRegionalServerCounterMetricDef),
				DialTestLogsInfoCounterMetric:            RegisterNewCounterVecWithRegister(register[0], DialTestLogsInfoCounterMetricDef),

				AWSAPIRequestCounterMetric:      RegisterNewCounterVecWithRegister(register[0], AWSAPIRequestCounterMetricDef),
				GCPAPIRequestCounterMetric:      RegisterNewCounterVecWithRegister(register[0], GCPAPIRequestCounterMetricDef),
				AliCloudAPIRequestCounterMetric: RegisterNewCounterVecWithRegister(register[0], AliCloudAPIRequestCounterMetricDef),
			}
		} else {
			metrics = &Metrics{
				BootTimeGaugeMetric: RegisterNewGaugeVec(BootTimeGaugeMetricDef),

				APIRequestsCounterMetric:       RegisterNewCounterVec(APIRequestsCounterMetricDef),
				RequestDurationHistogramMetric: RegisterNewHistogramVec(RequestDurationHistogramMetricDef),
				RequestSizeHistogramMetric:     RegisterNewHistogramVec(RequestSizeHistogramMetricDef),
				ResponseSizeHistogramMetric:    RegisterNewHistogramVec(ResponseSizeHistogramMetricDef),

				ErrorHandlerTotalCounterMetric: RegisterNewCounterVec(BizErrorTotalCounterMetricDef),

				GlobalRoutingRegionalTotalGaugeMetric: RegisterNewGaugeVec(GlobalRoutingRegionalTotalGaugeMetricDef),
				GlobalRoutingRegionalInfoGaugeMetric:  RegisterNewGaugeVec(GlobalRoutingRegionalInfoGaugeMetricDef),

				RegionalControllerRunsTotalCounterMetric: RegisterNewCounterVec(RegionalControllerRunsTotalCounterMetricDef),

				ErrorDialTestMetricsInfoCounterMetric:    RegisterNewCounterVec(ErrorDialTestMetricsInfoCounterMetricDef),
				ErrorDialTestRegionalServerCounterMetric: RegisterNewCounterVec(ErrorDialTestRegionalServerCounterMetricDef),
				DialTestLogsInfoCounterMetric:            RegisterNewCounterVec(DialTestLogsInfoCounterMetricDef),

				AWSAPIRequestCounterMetric:      RegisterNewCounterVec(AWSAPIRequestCounterMetricDef),
				GCPAPIRequestCounterMetric:      RegisterNewCounterVec(GCPAPIRequestCounterMetricDef),
				AliCloudAPIRequestCounterMetric: RegisterNewCounterVec(AliCloudAPIRequestCounterMetricDef),
			}
		}
	})
}

func SetupCloudAPIMetrics(register prometheus.Registerer) {
	once.Do(func() {
		metrics = &Metrics{
			AWSAPIRequestCounterMetric:      RegisterNewCounterVec(AWSAPIRequestCounterMetricDef),
			GCPAPIRequestCounterMetric:      RegisterNewCounterVec(GCPAPIRequestCounterMetricDef),
			AliCloudAPIRequestCounterMetric: RegisterNewCounterVec(AliCloudAPIRequestCounterMetricDef),
		}
	})
}

// HandleGlobalRoutingRegionalTotalMetrics
// @Description: handle global routing regional total metric
// @Parameter vendor
func HandleGlobalRoutingRegionalTotalMetrics(vendor string, total int) {
	GetMetrics().GlobalRoutingRegionalTotalGaugeMetric.With(prometheus.Labels{
		ServiceLabel: constants.ObservabilityGlobalServer,
		VendorLabel:  vendor,
	}).Set(float64(total))
}

// HandleGlobalRoutingRegionalInfoMetrics
// @Description: handle global routing regional info metric
// @Parameter vendor
// @Parameter region
func HandleGlobalRoutingRegionalInfoMetrics(vendor, region string) {
	GetMetrics().GlobalRoutingRegionalInfoGaugeMetric.With(prometheus.Labels{
		ServiceLabel: constants.ObservabilityGlobalServer,
		VendorLabel:  vendor,
		RegionLabel:  region,
	}).Set(float64(1))
}

// HandleRegionalControllerRunsMetrics
// @Description: handle regional controller runs metrics
// @Parameter vendor
// @Parameter region
// @Parameter clusterId
// @Parameter pullInterval
// @Parameter errorMsg
func HandleRegionalControllerRunsMetrics(vendor, region, contollername, resourcename, pullInterval, errorMsg string) {
	GetMetrics().RegionalControllerRunsTotalCounterMetric.With(prometheus.Labels{
		ServiceLabel:        constants.ObservabilityRegionalServer,
		VendorLabel:         vendor,
		RegionLabel:         region,
		ResourceNameLabel:   resourcename,
		ControllerNameLabel: contollername,
		PullIntervalLabel:   pullInterval,
		ErrorMessageLabel:   errorMsg,
	}).Inc()
}

// HandleGlobalBizErrorMetrics
// @Description: handle global biz error metrics
// @Parameter errMsg
func HandleGlobalBizErrorMetrics(errMsg string) {
	handleBizErrorMetrics(constants.ObservabilityGlobalServer, errMsg)
}

// HandleRegionalBizErrorMetrics
// @Description: handle regional biz error metrics
// @Parameter errMsg
func HandleRegionalBizErrorMetrics(errMsg string) {
	handleBizErrorMetrics(constants.ObservabilityRegionalServer, errMsg)
}

// handleBizErrorMetrics
// @Description: handle error handler metrics
// @Parameter serviceName
// @Parameter errMsg
func handleBizErrorMetrics(serviceName string, errMsg string) {
	if pc, file, line, ok := runtime.Caller(2); ok {
		fileNames := strings.Split(file, "/")
		funcNames := strings.Split(runtime.FuncForPC(pc).Name(), "/")
		GetMetrics().ErrorHandlerTotalCounterMetric.With(prometheus.Labels{
			ServiceLabel:      serviceName,
			FileNameLabel:     fmt.Sprintf("%s:%d", strings.TrimRight(fileNames[len(fileNames)-1], "/"), line),
			FuncNameLabel:     strings.TrimRight(funcNames[len(funcNames)-1], "/"),
			ErrorMessageLabel: errMsg,
		}).Inc()
	}
}

// HandleGlobalAPIMetrics
// @Description: Handle global api metrics
// @Parameter metricsType
// @return gin.HandlerFunc
func HandleGlobalAPIMetrics(metricsType constants.ApiEndpoint) gin.HandlerFunc {
	return func(c *gin.Context) {
		handleHTTPMetrics(c, constants.ObservabilityGlobalServer, metricsType)
	}
}

// HandleRegionalAPIMetrics
// @Description: Handle regional api metrics
// @Parameter metricsType
// @return gin.HandlerFunc
func HandleRegionalAPIMetrics(metricsType constants.ApiEndpoint) gin.HandlerFunc {
	return func(c *gin.Context) {
		handleHTTPMetrics(c, constants.ObservabilityRegionalServer, metricsType)
	}
}

// handleHTTPMetrics
// @Description: handle http metrics
// @Parameter c
// @Parameter serviceName
// @Parameter metricsType
func handleHTTPMetrics(c *gin.Context, serviceName string, metricsType constants.ApiEndpoint) {
	start := time.Now()
	c.Next()
	duration := time.Since(start)
	code := fmt.Sprintf("%d", c.Writer.Status())

	var vendor string
	var region string
	if obj, ok := c.Get(structs.GlobalServerRequestParametersKey); ok {
		reqParams := obj.(structs.RequestParameters)
		vendor = string(reqParams.Vendor)
		region = reqParams.Region
	}

	GetMetrics().APIRequestsCounterMetric.
		With(prometheus.Labels{ServiceLabel: serviceName, HandlerLabel: string(metricsType), MethodLabel: c.Request.Method, CodeLabel: code, VendorLabel: vendor, RegionLabel: region}).
		Inc()
	GetMetrics().RequestDurationHistogramMetric.
		With(prometheus.Labels{ServiceLabel: serviceName, HandlerLabel: string(metricsType), MethodLabel: c.Request.Method, CodeLabel: code, VendorLabel: vendor, RegionLabel: region}).
		Observe(duration.Seconds())
	GetMetrics().RequestSizeHistogramMetric.
		With(prometheus.Labels{ServiceLabel: serviceName, HandlerLabel: string(metricsType), MethodLabel: c.Request.Method, CodeLabel: code, VendorLabel: vendor, RegionLabel: region}).
		// statistics by kb
		Observe(float64(computeApproximateRequestSize(c.Request)) / 1024)
	GetMetrics().ResponseSizeHistogramMetric.
		With(prometheus.Labels{ServiceLabel: serviceName, HandlerLabel: string(metricsType), MethodLabel: c.Request.Method, CodeLabel: code, VendorLabel: vendor, RegionLabel: region}).
		// statistics by kb
		Observe(float64(c.Writer.Size()) / 1024)
}

// HandleAWSAPIRequestMetrics
// @Description: handle aws api request metrics
// @Parameter region
// @Parameter client
// @Parameter api
// @Parameter resourceName
// @Parameter errMsg
func HandleAWSAPIRequestMetrics(region, client, api, resourceName, errMsg string) {
	GetMetrics().AWSAPIRequestCounterMetric.With(prometheus.Labels{
		RegionLabel:       region,
		AWSClientLabel:    client,
		AWSAPILabel:       api,
		ResourceNameLabel: resourceName,
		ErrorMessageLabel: errMsg,
	}).Inc()
}

// HandleAliCloudAPIRequestMetrics
// @Description: handle ali cloud api request metrics
// @Parameter region
// @Parameter client
// @Parameter api
// @Parameter resourceName
// @Parameter errMsg
func HandleAliCloudAPIRequestMetrics(region, client, api, resourceName, errMsg string) {
	GetMetrics().AliCloudAPIRequestCounterMetric.With(prometheus.Labels{
		RegionLabel:         region,
		AliCloudClientLabel: client,
		AliCloudAPILabel:    api,
		ResourceNameLabel:   resourceName,
		ErrorMessageLabel:   errMsg,
	}).Inc()
}

// HandleGCPAPIRequestMetrics
// @Description: handle gcp api request metrics
// @Parameter region
// @Parameter client
// @Parameter api
// @Parameter resourceName
// @Parameter errMsg
func HandleGCPAPIRequestMetrics(region, client, api, resourceName, errMsg string) {
	GetMetrics().GCPAPIRequestCounterMetric.With(prometheus.Labels{
		RegionLabel:       region,
		GCPClientLabel:    client,
		GCPAPILabel:       api,
		ResourceNameLabel: resourceName,
		ErrorMessageLabel: errMsg,
	}).Inc()
}

// HandleDialTestMetricsErrorInfoMetrics
// @Description: handle dial test metrics error info metrics
// @Parameter vendor
// @Parameter region
// @Parameter clusterID
// @Parameter tenantID
// @Parameter projectID
// @Parameter applicationID
func HandleDialTestMetricsErrorInfoMetrics(vendor, region, clusterID, tenantID, projectID, applicationID string) {
	GetMetrics().ErrorDialTestMetricsInfoCounterMetric.With(prometheus.Labels{
		VendorLabel:        vendor,
		RegionLabel:        region,
		O11YClusterIDLabel: clusterID,
		TenantIDLabel:      tenantID,
		ProjectIDLabel:     projectID,
		ApplicationIDLabel: applicationID,
	}).Inc()
}

// HandleDialTestRegionalServerErrorMetrics
// @Description: handle dial test regional server error info metrics
// @Parameter service
// @Parameter vendor
// @Parameter region
func HandleDialTestRegionalServerErrorMetrics(vendor, region string) {
	GetMetrics().ErrorDialTestRegionalServerCounterMetric.With(prometheus.Labels{
		ServiceLabel: constants.ObservabilityRegionalServer,
		VendorLabel:  vendor,
		RegionLabel:  region,
	}).Inc()
}

// HandleDialTestLogsInfoMetrics
// @Description: handle dial test logs info metrics
// @Parameter vendor
// @Parameter region
// @Parameter clusterID
// @Parameter tenantID
// @Parameter projectID
// @Parameter applicationID
// @Parameter minutesToLastModified
func HandleDialTestLogsInfoMetrics(vendor, region, clusterID, tenantID, projectID, applicationID, minutesToLastModified string) {
	GetMetrics().DialTestLogsInfoCounterMetric.With(prometheus.Labels{
		VendorLabel:                vendor,
		RegionLabel:                region,
		O11YClusterIDLabel:         clusterID,
		TenantIDLabel:              tenantID,
		ProjectIDLabel:             projectID,
		ApplicationIDLabel:         applicationID,
		MinutesToLastModifiedLabel: minutesToLastModified,
	}).Inc()
}

// RegisterNewGaugeVec
// @Description: register new gauge vector
// @Parameter metricDef
// @return *prometheus.GaugeVec
func RegisterNewGaugeVec(metricDef MetricDef) *prometheus.GaugeVec {
	metric := prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: constants.ObservabilityBaseServiceName,
			Name:      metricDef.Name,
			Help:      metricDef.Help,
		},
		metricDef.LabelNames,
	)
	prometheus.MustRegister(metric)
	return metric
}

// RegisterNewCounterVec
// @Description: register new counter vector
// @Parameter metricDef
// @return *prometheus.CounterVec
func RegisterNewCounterVec(metricDef MetricDef) *prometheus.CounterVec {
	metric := prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: constants.ObservabilityBaseServiceName,
			Name:      metricDef.Name,
			Help:      metricDef.Help,
		},
		metricDef.LabelNames,
	)
	prometheus.MustRegister(metric)
	return metric
}

// RegisterNewHistogramVec
// @Description: register new histogram vector
// @Parameter metricDef
// @return *prometheus.HistogramVec
func RegisterNewHistogramVec(metricDef MetricDef) *prometheus.HistogramVec {
	metric := prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: constants.ObservabilityBaseServiceName,
			Name:      metricDef.Name,
			Help:      metricDef.Help,
		},
		metricDef.LabelNames,
	)
	prometheus.MustRegister(metric)
	return metric
}

// RegisterNewSummaryVec
// @Description: register new summary vector
// @Parameter metricDef
// @return *prometheus.SummaryVec
func RegisterNewSummaryVec(metricDef MetricDef) *prometheus.SummaryVec {
	metric := prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Namespace: constants.ObservabilityBaseServiceName,
			Name:      metricDef.Name,
			Help:      metricDef.Help,
		},
		metricDef.LabelNames,
	)
	prometheus.MustRegister(metric)
	return metric
}

// RegisterNewGaugeVecWithRegister
// @Description: register new gauge vector with customer register
// @Parameter register
// @Parameter metricDef
// @return *prometheus.GaugeVec
func RegisterNewGaugeVecWithRegister(register prometheus.Registerer, metricDef MetricDef) *prometheus.GaugeVec {
	metric := prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: constants.ObservabilityBaseServiceName,
			Name:      metricDef.Name,
			Help:      metricDef.Help,
		},
		metricDef.LabelNames,
	)
	register.MustRegister(metric)
	return metric
}

// RegisterNewCounterVecWithRegister
// @Description: register new counter vector with customer register
// @Parameter register
// @Parameter metricDef
// @return *prometheus.CounterVec
func RegisterNewCounterVecWithRegister(register prometheus.Registerer, metricDef MetricDef) *prometheus.CounterVec {
	metric := prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: constants.ObservabilityBaseServiceName,
			Name:      metricDef.Name,
			Help:      metricDef.Help,
		},
		metricDef.LabelNames,
	)
	register.MustRegister(metric)
	return metric
}

// RegisterNewHistogramVecWithRegister
// @Description: register new histogram vector with customer register
// @Parameter register
// @Parameter metricDef
// @return *prometheus.HistogramVec
func RegisterNewHistogramVecWithRegister(register prometheus.Registerer, metricDef MetricDef) *prometheus.HistogramVec {
	metric := prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: constants.ObservabilityBaseServiceName,
			Name:      metricDef.Name,
			Help:      metricDef.Help,
		},
		metricDef.LabelNames,
	)
	register.MustRegister(metric)
	return metric
}

// RegisterNewSummaryVecWithRegister
// @Description: register new summary vector with customer register
// @Parameter register
// @Parameter metricDef
// @return *prometheus.SummaryVec
func RegisterNewSummaryVecWithRegister(register prometheus.Registerer, metricDef MetricDef) *prometheus.SummaryVec {
	metric := prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Namespace: constants.ObservabilityBaseServiceName,
			Name:      metricDef.Name,
			Help:      metricDef.Help,
		},
		metricDef.LabelNames,
	)
	register.MustRegister(metric)
	return metric
}

// computeApproximateRequestSize
// @Description: compute approximate request size
// @Parameter r
// @return int
func computeApproximateRequestSize(r *http.Request) int {
	s := 0
	if r.URL != nil {
		s = len(r.URL.Path)
	}

	s += len(r.Method)
	s += len(r.Proto)
	for name, values := range r.Header {
		s += len(name)
		for _, value := range values {
			s += len(value)
		}
	}
	s += len(r.Host)

	if r.ContentLength != -1 {
		s += int(r.ContentLength)
	}
	return s
}
