package api

import (
	"bytes"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httputil"
	url2 "net/url"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap-inc/clinic/pkg/api/cloud"
	"github.com/pingcap-inc/clinic/pkg/api/common"
	"github.com/pingcap-inc/clinic/pkg/o11y"

	proxy2 "github.com/pingcap-inc/clinic/pkg/proxy"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"

	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/cache"
	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/es"
	"github.com/pingcap-inc/clinic/pkg/importer/metrics"
	ctxlog "github.com/pingcap-inc/clinic/pkg/log"
	rbac2 "github.com/pingcap-inc/clinic/pkg/rbac"
)

const (
	URLReferer = "Referer"
	// grafana
	GrafanaPrefix = "grafana"

	GrafanaTiDBClusterCookie      = "clinic-tidbcluster"
	GrafanaProxyCookieName        = "clinic.current_org"
	GrafanaProxyOrgTypeCookie     = "clinic.current_orgType"
	GrafanaProjectCookie          = "clinic.project"
	GrafanaControlPlaneInfoCookie = "clinic.control_plane_info"

	// grafana provisioning datasources
	// datasource 1: Prometheus
	MetricQueryPrefix = "/metric/query"
	// datasource 2: Prometheus-fake
	TiDBClusterSeriesQueryPrefix = "/metric/series"
	// datasource 5: o11y-global-variables
	O11yInfraSeriesQueryPrefix = "/metric/o11y_infra/series"
	// datasource 6: o11y-global-data
	O11yMetricQueryPrefix = "/metric/o11y/query"
	// datasource 7: base-metric-data
	BaseMetricQueryPrefix = "/metric/base/query"
	// datasource 8: cloud-node-metric
	O11yNodeMetricQueryPrefix = "/metric/node/query"
	// datasource 9: project-metric
	ProjectMetricQueryPrefix = "/metric/project/query"
	// datasource: o11y-loki-data
	LogQueryPrefix = "/log/query"

	// kibana
	KibanaPrefix    = "kibana"
	kibanaTenantKey = "security_tenant"
)

// var allowGrafanaAPI = map[string]bool{
// 	fmt.Sprintf("/%s/api/frontend-metrics", GrafanaPrefix): false,
// 	// fmt.Sprintf("/%s/api/datasources/proxy/1/api/v1/series", GrafanaPrefix):      false,
// 	// fmt.Sprintf("/%s/api/datasources/proxy/4/api/v1/series", GrafanaPrefix):      false,
// 	// fmt.Sprintf("/%s/api/datasources/proxy/2/api/v1/series", GrafanaPrefix):      false,
// 	fmt.Sprintf("/%s/api/ds/query", GrafanaPrefix):                                   false,
// 	fmt.Sprintf("/%s/api/live/ws", GrafanaPrefix):                                    false,
// 	fmt.Sprintf("/%s/api/live/publish", GrafanaPrefix):                               false,
// 	fmt.Sprintf("/%s/api/search", GrafanaPrefix):                                     false,
// 	fmt.Sprintf("/%s/api/annotations", GrafanaPrefix):                                true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/1/api/v1/query_range", GrafanaPrefix):     true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/1/api/v1/query", GrafanaPrefix):           true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/1/api/v1/metadata", GrafanaPrefix):        true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/1/api/v1/labels", GrafanaPrefix):          true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/1/api/v1/query_exemplars", GrafanaPrefix): true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/2/api/v1/query_range", GrafanaPrefix):     true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/2/api/v1/query", GrafanaPrefix):           true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/4/_msearch", GrafanaPrefix):               true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/6/api/v1/query_range", GrafanaPrefix):     true,
// 	fmt.Sprintf("/%s/api/datasources/proxy/6/api/v1/query", GrafanaPrefix):           true,
// }

var allowKibanaAPI = map[string]bool{
	fmt.Sprintf("/%s/app/discover", KibanaPrefix):                 false,
	fmt.Sprintf("/%s/api/saved_objects/_bulk_get", KibanaPrefix):  false,
	fmt.Sprintf("/%s/api/v1/configuration/account", KibanaPrefix): false,
	fmt.Sprintf("/%s/api/v1/restapiinfo", KibanaPrefix):           false,
	fmt.Sprintf("/%s/api/saved_objects/_find", KibanaPrefix):      false,
	fmt.Sprintf("/%s/api/core/capabilities", KibanaPrefix):        false,

	fmt.Sprintf("/%s/api/index_patterns/_fields_for_wildcard", KibanaPrefix): false,
	fmt.Sprintf("/%s/api/saved_objects/index-pattern", KibanaPrefix):         false,
	fmt.Sprintf("/%s/internal/search/opensearch", KibanaPrefix):              true,
}

func InitProxy(
	engine *gin.Engine,
	sessionManager *session.SessionManager,
	rbac rbac2.Auth,
	cfg config.SinkConfig,
	metricProxyHandler *proxy2.TiDBClusterMetricHandler,
	o11yProxyHandler *proxy2.O11yProxyHandler,
	nodeMetricProxyHandler *proxy2.CloudNodeMetricHandler,
	projectMetricProxyHandler *proxy2.ProjectMetricHandler,
) {
	kibanaRouter := engine.Group("/kibana")
	kibanaRouter.Use(sessionManager.MWTryAuth)
	kibanaRouter.Any("/*any", func(context *gin.Context) {
		user := session.GetUser(context)
		if user == nil {
			_ = context.Error(error2.ErrNotFound.New("the user is not found"))
			return
		}

		oidStr := getValueFromReferer(context.GetHeader(URLReferer), kibanaTenantKey, func() url2.Values {
			return context.Request.URL.Query()
		})
		oid := es.GetOrgFromName(oidStr)
		if oid <= 0 {
			_ = context.Error(error2.ErrNotFound.New("not found the organization"))
			return
		}

		ok, err := rbac.Auth(user.UserID, user.Internal, rbac2.Cluster(), rbac2.ReadActing(), oid)
		if err != nil || !ok {
			_ = context.Error(error2.ErrForbidden.New("permission deny"))
			return
		}

		kibanaUser := cache.KibanaCache.Get(fmt.Sprintf("%d", oid))
		if kibanaUser == nil {
			_ = context.Error(error2.ErrInternal.New("internal error"))
			return
		}

		kibanaProxy(context, user, oid, fmt.Sprintf("%s:%d", cfg.LogConfig.KibanaConfig.Host, cfg.LogConfig.KibanaConfig.Port), kibanaUser.Name, kibanaUser.Password)
	})

	grafanaRouter := engine.Group("/" + GrafanaPrefix)
	grafanaRouter.Use(sessionManager.MWTryAuth)
	grafanaRouter.Any("/*any", func(c *gin.Context) {
		user := session.GetUser(c)
		if user == nil {
			_ = c.Error(error2.ErrNotFound.New("the user is not found"))
			return
		}
		// set org id to cookie
		org := getValueFromReferer(c.GetHeader(URLReferer), "var-org", func() url2.Values {
			return c.Request.URL.Query()
		})
		if org == "" {
			// for report, record org_id in query
			org = c.Query("org_id")
		}
		if org != "" {
			c.Request.AddCookie(&http.Cookie{
				Name:     GrafanaProxyCookieName,
				Value:    org,
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
		}
		// add org type to cookie
		orgType := getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarOrgTypeVariable, func() url2.Values {
			return c.Request.URL.Query()
		})

		if orgType != "" {
			c.Request.AddCookie(&http.Cookie{
				Name:     GrafanaProxyOrgTypeCookie,
				Value:    orgType,
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
		}

		// add control_plane_info to cookie
		controlPlaneInfo := getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarControlPlaneInfoVariable, func() url2.Values {
			return c.Request.URL.Query()
		})
		if controlPlaneInfo != "" {
			c.Request.AddCookie(&http.Cookie{
				Name:     GrafanaControlPlaneInfoCookie,
				Value:    controlPlaneInfo,
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
		}

		// add cluster id to cookie
		clusterID := getGrafanaClusterIDFromRequest(c)
		if clusterID != "" {
			c.Request.AddCookie(&http.Cookie{
				Name:     GrafanaTiDBClusterCookie,
				Value:    clusterID,
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
		}

		// add project id to cookie
		projectID := getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarProjectIDVariable, func() url2.Values {
			return c.Request.URL.Query()
		})
		if projectID != "" {
			c.Request.AddCookie(&http.Cookie{
				Name:     GrafanaProjectCookie,
				Value:    projectID,
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
		}

		version := getValueFromReferer(c.GetHeader(URLReferer), "var-tidb_version", func() url2.Values {
			return c.Request.URL.Query()
		})

		if cfg.MetricConfig.EnableInternalGrafana && user.Internal {
			version = version + "-internal"
		}

		ep, ok := cfg.MetricConfig.GrafanaEndpoints[version]
		if !ok {
			if cfg.MetricConfig.EnableInternalGrafana && user.Internal && c.Query("force-grafana7") == "" {
				ep = cfg.MetricConfig.GrafanaEndpoints["master-internal"]
			} else {
				ep = cfg.MetricConfig.GrafanaEndpoints["master"]
			}
		}

		grafanaProxy(c, ep, cfg.MetricConfig.EnableInternalGrafana && user.Internal)
	})

	vmQueryRouter := engine.Group(MetricQueryPrefix)
	vmQueryRouter.Use(sessionManager.MWTryAuth)
	vmQueryRouter.Any("/*any", func(c *gin.Context) {
		metricProxyHandler.GinHandle(c,
			getOrgTypeFromCookie,
			getOrgIDFromCookie,
			getClusterIDFromCookie,
			getControlPlaneInfoFromRequest,
			func(s string) string {
				return strings.TrimPrefix(s, MetricQueryPrefix)
			})
	})

	logQueryHandler := func(c *gin.Context) {
		metricProxyHandler.GinHandleLogs(c,
			getOrgTypeFromCookie,
			getOrgIDFromCookie,
			getLogQueryClusterIDFromRequest,
			trimLogQueryRoutePrefix)
	}
	registerLogQueryRoutes := func(router *gin.RouterGroup) {
		router.Any("/loki/api/v1/query", logQueryHandler)
		router.Any("/loki/api/v1/query_range", logQueryHandler)
		router.GET("/loki/api/v1/labels", logQueryHandler)
		router.GET("/loki/api/v1/label/:name/values", logQueryHandler)
		router.GET("/loki/api/v1/series", logQueryHandler)
	}
	logQueryRouter := engine.Group(LogQueryPrefix)
	logQueryRouter.Use(sessionManager.MWTryAuth)
	registerLogQueryRoutes(logQueryRouter)

	// cloud-node-metric
	// nodeQueryRouter forward query and query range request to o11y ds service
	nodeQueryRouter := engine.Group(O11yNodeMetricQueryPrefix)
	nodeQueryRouter.Use(sessionManager.MWTryAuth)
	nodeQueryRouter.Any("/*any", func(c *gin.Context) {
		nodeMetricProxyHandler.GinHandle(c,
			func(c *gin.Context) string {
				clusterID, err := c.Cookie(GrafanaTiDBClusterCookie)
				if err != nil {
					return ""
				}
				clusterInfo := strings.Split(clusterID, "/")
				if len(clusterInfo) == 2 {
					clusterID = clusterInfo[0]
				}
				// debug
				fmt.Println("cloud-node-metric: clusterID", clusterID)
				return clusterID
			},
			func(s string) string {
				return strings.TrimPrefix(s, O11yNodeMetricQueryPrefix)
			})
	})

	// project-metric
	// projectQueryRouter forward query and query range request to o11y ds service
	projectQueryRouter := engine.Group(ProjectMetricQueryPrefix)
	projectQueryRouter.Use(sessionManager.MWTryAuth)
	projectQueryRouter.Any("/*any", func(c *gin.Context) {
		projectMetricProxyHandler.GinHandle(c,
			func(c *gin.Context) string {
				clusterID, err := c.Cookie(GrafanaTiDBClusterCookie)
				if err != nil {
					return ""
				}
				clusterInfo := strings.Split(clusterID, "/")
				if len(clusterInfo) == 2 {
					clusterID = clusterInfo[0]
				}
				// debug
				fmt.Println("project-metric: clusterID", clusterID)
				return clusterID
			},
			func(c *gin.Context) string {
				projectID, err := c.Cookie(GrafanaProjectCookie)
				// debug
				fmt.Println("project-metric: projectID", projectID)
				if err != nil {
					return ""
				}
				return projectID
			},
			func(c *gin.Context) string {
				controlPlaneInfo, err := c.Cookie(GrafanaControlPlaneInfoCookie)
				// debug
				fmt.Println("project-metric: controlPlaneInfo", controlPlaneInfo)
				if err != nil {
					return ""
				}
				return controlPlaneInfo
			},
			func(s string) string {
				return strings.TrimPrefix(s, ProjectMetricQueryPrefix)
			})
	})

	// o11y-global-data
	// o11yQueryRouter forward query and query range request to o11y ds service
	o11yQueryRouter := engine.Group(O11yMetricQueryPrefix)
	o11yQueryRouter.Use(sessionManager.MWTryAuth)
	o11yQueryRouter.Use(cloud.CloudPermission)
	o11yQueryRouter.Any("/*any", func(c *gin.Context) {
		o11yProxyHandler.GinHandle(c,
			func(c *gin.Context) string {
				return o11yProxyHandler.GetO11yClusterInfo(c)
			},
			func(c *gin.Context) string {
				return getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarControlPlaneInfoVariable, func() url2.Values {
					return c.Request.URL.Query()
				})
			},
			func(s string) string {
				return strings.TrimPrefix(s, O11yMetricQueryPrefix)
			},
			getO11yMetricClusterIDFromRequest)
	})

	// base-metric-data
	// baseQueryRouter forward query and query range request to o11y ds service
	baseQueryRouter := engine.Group(BaseMetricQueryPrefix)
	baseQueryRouter.Use(sessionManager.MWTryAuth)
	baseQueryRouter.Use(cloud.CloudPermission)
	baseQueryRouter.Any("/*any", func(c *gin.Context) {
		o11yProxyHandler.GinHandle(c,
			func(c *gin.Context) string {
				ctx := c.Request.Context()
				clusters, err := o11yProxyHandler.ListBaseClusterInfo(c.Request.Context())
				if err != nil {
					ctxlog.Error(ctx, "get base cluster via o11y failed", zap.Error(err))
					return ""
				}

				if len(clusters) == 1 {
					o11yClusterInfo := fmt.Sprintf("%s/%s/%s/%s", clusters[0].ProjectID, clusters[0].Vendor, clusters[0].Region, clusters[0].ID)
					ctxlog.Info(ctx, "get base cluster via o11y", zap.String("o11y_cluster_info", o11yClusterInfo))
					return o11yClusterInfo
				}

				ctxlog.Error(ctx, "get mult base cluster via o11y failed", zap.Error(err))

				return ""
			},
			func(c *gin.Context) string {
				return getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarControlPlaneInfoVariable, func() url2.Values {
					return c.Request.URL.Query()
				})
			},
			func(s string) string {
				return strings.TrimPrefix(s, BaseMetricQueryPrefix)
			},
			getO11yMetricClusterIDFromRequest)
	})

	// Prometheus-fake
	tidbClusterSeriesRouter := engine.Group(TiDBClusterSeriesQueryPrefix)
	tidbClusterSeriesRouter.Use(sessionManager.MWTryAuth)
	tidbClusterSeriesRouter.Any("/*any", func(c *gin.Context) {
		user := session.GetUser(c)
		if user == nil {
			_ = c.Error(error2.ErrNotFound.New("the user is not found"))
			return
		}

		clusterID, err := c.Cookie(GrafanaTiDBClusterCookie)
		if err != nil {
			_ = c.Error(error2.ErrNotFound.New("cluster is not found"))
			return
		}

		c.JSON(http.StatusOK, &PromResponse{
			Status: "success",
			Data: []map[string]string{
				{
					metrics.ClusterLabel: clusterID,
				},
			},
		})
	})

	// o11y-global-variables
	// o11yInfraSeriesRouter used to mock series query result for global cluster for o11y
	o11yInfraSeriesRouter := engine.Group(O11yInfraSeriesQueryPrefix)
	o11yInfraSeriesRouter.Use(sessionManager.MWTryAuth)
	o11yInfraSeriesRouter.Use(cloud.CloudPermission)
	o11yInfraSeriesRouter.Any("/prometheus/api/v1/series", func(c *gin.Context) {
		user := session.GetUser(c)
		if user == nil {
			_ = c.Error(error2.ErrNotFound.New("the user is not found"))
			return
		}
		var err error
		var clusters []o11y.ClusterInfo
		switch proxy2.GetO11yAppID(proxy2.GetO11yPromQL(c)) {
		case o11y.GlobalDevtierID:
			// query o11y to list global devtier cluster.
			if clusters, err = o11yProxyHandler.ListDevTierClusterInfo(c.Request.Context()); err != nil {
				_ = c.Error(error2.ErrNotFound.New("query global devtier cluster info failed"))
				return
			}
		case o11y.BaseID:
			// query o11y to list base cluster.
			if clusters, err = o11yProxyHandler.ListBaseClusterInfo(c.Request.Context()); err != nil {
				_ = c.Error(error2.ErrNotFound.New("query base cluster info failed"))
				return
			}
		case o11y.SeedID:
			// query o11y to list seed cluster.
			if clusters, err = o11yProxyHandler.ListSeedClusterInfo(c.Request.Context()); err != nil {
				_ = c.Error(error2.ErrNotFound.New("query seed cluster info failed"))
				return
			}
		default:
			// query o11y to list all global cluster.
			if clusters, err = o11yProxyHandler.ListGlobalClusterInfo(c.Request.Context()); err != nil {
				_ = c.Error(error2.ErrNotFound.New("query global cluster info failed"))
				return
			}
		}

		data := make([]map[string]string, 0, len(clusters))
		region := proxy2.GetO11yRegion(proxy2.GetO11yPromQL(c))
		for _, cls := range clusters {
			if region == "" || cls.Region == region {
				data = append(data,
					map[string]string{
						"o11y_cluster_info":      fmt.Sprintf("%s/%s/%s/%s", cls.ProjectID, cls.Vendor, cls.Region, cls.ID),
						"o11y_tenant_id":         cls.TenantID,
						"o11y_project_id":        cls.ProjectID,
						"o11y_application_id":    cls.ApplicationID,
						"o11y_id":                cls.ID,
						"o11y_region":            cls.Region,
						"o11y_provider":          cls.Vendor,
						"o11y_internal_read_uri": cls.InternalReadUri,
						"control_plane_info":     fmt.Sprintf("%s/%s-%s/%s", cls.ProjectID, cls.Vendor, cls.Region, cls.ApplicationID),
					})
			}
		}
		c.JSON(http.StatusOK, &PromResponse{
			Status: "success",
			Data:   data,
		})
	})
}

func getValueFromReferer(referer, orgKey string, req func() url2.Values) string {
	if referer == "" {
		return req().Get(orgKey)
	}

	u, err := url2.Parse(referer)
	if err != nil {
		return req().Get(orgKey)
	}

	v := u.Query().Get(orgKey)
	if v == "" {
		return req().Get(orgKey)
	}
	return v
}

func getOrgIDFromCookie(c *gin.Context) uint {
	orgIDStr, err := c.Cookie(GrafanaProxyCookieName)
	if err != nil {
		return 0
	}

	orgID, err := strconv.ParseUint(orgIDStr, 10, 64)
	if err != nil {
		ctxlog.Error(c.Request.Context(), "unexpected cookie", zap.String("oid", orgIDStr))
		return 0
	}
	return uint(orgID)
}

func getOrgTypeFromCookie(c *gin.Context) string {
	orgType, err := c.Cookie(GrafanaProxyOrgTypeCookie)
	if err != nil {
		return ""
	}

	return orgType
}

func getClusterIDFromCookie(c *gin.Context) string {
	clusterID, err := c.Cookie(GrafanaTiDBClusterCookie)
	if err != nil {
		return ""
	}
	return clusterID
}

func getGrafanaClusterIDFromRequest(c *gin.Context) string {
	if clusterID := getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarTiDBClusterVariable, func() url2.Values {
		return c.Request.URL.Query()
	}); clusterID != "" {
		return clusterID
	}
	return getValueFromReferer(c.GetHeader(URLReferer), "var-cluster_id", func() url2.Values {
		return c.Request.URL.Query()
	})
}

func getO11yMetricClusterIDFromRequest(c *gin.Context) string {
	if clusterID := getGrafanaClusterIDFromRequest(c); clusterID != "" {
		return clusterID
	}
	return getClusterIDFromCookie(c)
}

func getLogQueryClusterIDFromRequest(c *gin.Context) string {
	if clusterID := c.Query("cluster_id"); clusterID != "" {
		return clusterID
	}
	if clusterID := c.GetHeader("x-clinic-cluster-id"); clusterID != "" {
		return clusterID
	}
	if clusterID := c.Query("var-cluster_id"); clusterID != "" {
		return clusterID
	}
	if clusterID := c.Query(proxy2.VarTiDBClusterVariable); clusterID != "" {
		return clusterID
	}
	if clusterID := getValueFromReferer(c.GetHeader(URLReferer), "var-cluster_id", func() url2.Values {
		return url2.Values{}
	}); clusterID != "" {
		return clusterID
	}
	if clusterID := getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarTiDBClusterVariable, func() url2.Values {
		return url2.Values{}
	}); clusterID != "" {
		return clusterID
	}
	return getClusterIDFromCookie(c)
}

func getLogQueryControlPlaneInfoFromRequest(c *gin.Context) string {
	if controlPlaneInfo := c.GetHeader(proxy2.ControlPlaneInfoVariable); controlPlaneInfo != "" {
		return controlPlaneInfo
	}
	if controlPlaneInfo := c.Query(proxy2.ControlPlaneInfoVariable); controlPlaneInfo != "" {
		return controlPlaneInfo
	}
	if controlPlaneInfo := c.Query(proxy2.VarControlPlaneInfoVariable); controlPlaneInfo != "" {
		return controlPlaneInfo
	}
	if controlPlaneInfo := getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarControlPlaneInfoVariable, func() url2.Values {
		return url2.Values{}
	}); controlPlaneInfo != "" {
		return controlPlaneInfo
	}
	controlPlaneInfo, err := c.Cookie(GrafanaControlPlaneInfoCookie)
	if err != nil {
		return ""
	}
	return controlPlaneInfo
}

func trimLogQueryRoutePrefix(s string) string {
	return strings.TrimPrefix(s, LogQueryPrefix)
}

func getControlPlaneInfoFromRequest(c *gin.Context) string {
	if controlPlaneInfo := c.GetHeader(proxy2.ControlPlaneInfoVariable); controlPlaneInfo != "" {
		return controlPlaneInfo
	}
	if controlPlaneInfo := c.Query(proxy2.ControlPlaneInfoVariable); controlPlaneInfo != "" {
		return controlPlaneInfo
	}
	if controlPlaneInfo := getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarControlPlaneInfoVariable, func() url2.Values {
		return c.Request.URL.Query()
	}); controlPlaneInfo != "" {
		return controlPlaneInfo
	}
	controlPlaneInfo, err := c.Cookie(GrafanaControlPlaneInfoCookie)
	if err != nil {
		return ""
	}
	return controlPlaneInfo
}

func kibanaProxy(c *gin.Context, u *session.CtxUser, orgID uint, host, userName, password string) {
	if u.Internal {
		proxy(c, host, func(r *http.Request) *http.Request {
			r.SetBasicAuth(userName, password)
			return r
		}, nil, nil)
		return
	}

	requestURI := c.Request.URL.Path
	if !strings.HasPrefix(requestURI, fmt.Sprintf("/%s/api", KibanaPrefix)) &&
		(strings.HasSuffix(requestURI, ".js") ||
			strings.HasSuffix(requestURI, ".json") ||
			strings.HasSuffix(requestURI, ".css") ||
			strings.HasPrefix(requestURI, fmt.Sprintf("/%s/ui", KibanaPrefix)) ||
			strings.HasPrefix(requestURI, fmt.Sprintf("%s/36526", KibanaPrefix))) && c.Request.Method == http.MethodGet {
		proxy(c, host, func(r *http.Request) *http.Request {
			r.SetBasicAuth(userName, password)
			return r
		}, nil, nil)
		return
	}

	_, ok := allowKibanaAPI[requestURI]
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	proxy(c, host, func(r *http.Request) *http.Request {
		r.SetBasicAuth(userName, password)
		r.Header.Set("securitytenant", es.GetOrgName(orgID))
		r.Header.Set("sgtenant", es.GetOrgName(orgID))
		return r
	}, nil, nil)
}

func proxy(c *gin.Context, host string,
	directorF func(r *http.Request) *http.Request,
	modifyResponse func(*http.Response) error,
	errorHandler func(http.ResponseWriter, *http.Request, error),
) {
	ctx := c.Request.Context()
	url := &url2.URL{
		Scheme: "http",
		Host:   host,
	}
	ctxlog.Info(ctx, "reverse proxy dispatch",
		zap.String("target", url.String()),
		zap.String("method", c.Request.Method),
		zap.String("path", c.Request.URL.Path))
	proxy := httputil.NewSingleHostReverseProxy(url)
	originDirector := proxy.Director
	director := func(req *http.Request) {
		req = directorF(req)
		originDirector(req)
	}

	proxy.Director = director
	proxy.ModifyResponse = modifyResponse
	proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
		ctxlog.Error(req.Context(), "reverse proxy failed",
			zap.Error(err),
			zap.String("method", req.Method),
			zap.String("path", req.URL.Path),
			zap.String("targetHost", req.URL.Host))
		if errorHandler != nil {
			errorHandler(rw, req, err)
		}
	}

	proxy.ServeHTTP(c.Writer, c.Request)
}

func grafanaProxy(c *gin.Context, host string, internalUser bool) {
	requestPath := c.Request.URL.Path
	ctx := c.Request.Context()
	if requestPath == fmt.Sprintf("/%s/api/ds/query", GrafanaPrefix) {
		ctxlog.Info(ctx, "grafana ds query received",
			zap.String("method", c.Request.Method),
			zap.String("path", requestPath),
			zap.String("targetHost", host))
	}
	if !strings.HasPrefix(requestPath, fmt.Sprintf("/%s/api", GrafanaPrefix)) &&
		c.Request.Method == http.MethodGet {
		proxy(c, host, func(r *http.Request) *http.Request {
			return r
		}, nil, nil)
		return
	}

	if strings.HasPrefix(requestPath, fmt.Sprintf("/%s/api/dashboards/uid", GrafanaPrefix)) {
		proxy(c, host, func(r *http.Request) *http.Request {
			return r
		}, nil, nil)
		return
	}

	if strings.HasPrefix(requestPath, fmt.Sprintf("/%s/api/datasources/proxy/1/api/v1/label", GrafanaPrefix)) {
		proxy(c, host, func(r *http.Request) *http.Request {
			return r
		}, nil, nil)
		return
	}
	// to support panel with alert rule. TiKV Detail panel uses alert rule
	if strings.HasPrefix(requestPath, fmt.Sprintf("/%s/api/alerts", GrafanaPrefix)) {
		proxy(c, host, func(r *http.Request) *http.Request {
			return r
		}, nil, nil)
		return
	}

	if strings.HasSuffix(requestPath, "api/v1/series") {
		proxy(c, host, func(r *http.Request) *http.Request {
			r.AddCookie(&http.Cookie{
				Name:     GrafanaTiDBClusterCookie,
				Value:    getGrafanaClusterIDFromRequest(c),
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
			r.AddCookie(&http.Cookie{
				Name: GrafanaProjectCookie,
				Value: getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarProjectIDVariable, func() url2.Values {
					return c.Request.URL.Query()
				}),
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
			r.AddCookie(&http.Cookie{
				Name: GrafanaControlPlaneInfoCookie,
				Value: getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarControlPlaneInfoVariable, func() url2.Values {
					return c.Request.URL.Query()
				}),
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})

			return r
		}, nil, nil)
		return
	}

	// check external user permission
	if !internalUser {
		user := session.GetUser(c)
		if user == nil {
			_ = c.Error(error2.ErrNotFound.New("the user is not found"))
			return
		}
		oid := getOrgIDFromCookie(c)
		if oid <= 0 {
			ctxlog.Error(ctx, "get org id from cookie failed", zap.String("requestPath", requestPath))
			_ = c.Error(error2.ErrNotFound.New("the organization is not found"))
			return
		}
		cid := getClusterIDFromCookie(c)
		if cid == "" {
			_ = c.Error(error2.ErrNotFound.New("cluster is not found"))
			return
		}

		ok, err := common.CheckExternalUserPermission(user, oid, cid)
		if err != nil || !ok {
			ctxlog.Error(ctx, "external user permission denied", zap.Error(err), zap.Bool("has permission", ok))
			_ = c.Error(error2.ErrForbidden.New("permission deny"))
			return
		}
	}

	// internal api
	// grafana/api/search
	if requestPath == fmt.Sprintf("/%s/api/search", GrafanaPrefix) {
		proxy(c,
			host,
			func(r *http.Request) *http.Request {
				return r
			},
			filterPrivateDashboard,
			nil)
		return
	}

	// for other grafana request, we also need to set the cookie.
	proxy(c,
		host,
		func(r *http.Request) *http.Request {
			r.AddCookie(&http.Cookie{
				Name:     GrafanaTiDBClusterCookie,
				Value:    getGrafanaClusterIDFromRequest(c),
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
			r.AddCookie(&http.Cookie{
				Name: GrafanaProjectCookie,
				Value: getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarProjectIDVariable, func() url2.Values {
					return c.Request.URL.Query()
				}),
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
			r.AddCookie(&http.Cookie{
				Name: GrafanaControlPlaneInfoCookie,
				Value: getValueFromReferer(c.GetHeader(URLReferer), proxy2.VarControlPlaneInfoVariable, func() url2.Values {
					return c.Request.URL.Query()
				}),
				MaxAge:   int(time.Hour * 24 / time.Second),
				Path:     "/",
				Domain:   "",
				Secure:   false,
				HttpOnly: false,
			})
			return r
		},
		nil, nil)
}

// filterPrivateDashboard
func filterPrivateDashboard(r *http.Response) error {
	// direct return, if response is not ok
	if r.StatusCode != http.StatusOK {
		return nil
	}

	var (
		useGzip bool
		content []byte
		err     error
	)

	if r.Header.Get("content-encoding") == "gzip" {
		useGzip = true
	}

	if useGzip {
		var gr *gzip.Reader
		if gr, err = gzip.NewReader(r.Body); err != nil {
			return err
		}
		if err := gr.Close(); err != nil {
			ctxlog.Error(r.Request.Context(), "gzip reader close failed", zap.Error(err))
		}

		content, err = io.ReadAll(gr)
	} else {
		content, err = io.ReadAll(r.Body)
	}
	if err != nil {
		return err
	}

	var rDashboards GrafanaSearchResponse
	fDashboards := GrafanaSearchResponse{} // can't be nill
	err = json.Unmarshal(content, &rDashboards)
	if err != nil {
		return err
	}

	for _, dashboard := range rDashboards {
		switch dashboard.Type {
		case "dash-folder":
			if !strings.HasPrefix(dashboard.Title, "clinic-") {
				fDashboards = append(fDashboards, dashboard)
			}
		case "dash-db":
			if !strings.HasPrefix(dashboard.FolderTitle, "clinic-") {
				fDashboards = append(fDashboards, dashboard)
			}
		}
	}

	content, err = json.Marshal(fDashboards)
	if err != nil {
		return err
	}

	buf := &bytes.Buffer{}
	if useGzip {
		gw := gzip.NewWriter(buf)
		if _, err := gw.Write(content); err != nil {
			return err
		}
		if err := gw.Close(); err != nil {
			ctxlog.Error(r.Request.Context(), "gzip writer close failed", zap.Error(err))
		}
	} else {
		buf.Write(content)
	}

	r.Body = io.NopCloser(buf)
	r.Header["Content-Length"] = []string{fmt.Sprint(buf.Len())}
	return nil
}
