package proxy

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httputil"
	"net/url"
	url2 "net/url"
	"path"
	"regexp"
	"slices"
	"strings"
	"time"

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

	"github.com/pingcap-inc/clinic/pkg/api/common"
	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/log"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	o11yurls "github.com/pingcap-inc/clinic/pkg/o11y/urls"
	rbac2 "github.com/pingcap-inc/clinic/pkg/rbac"
)

var o11yUswest2SwitchStagingDate, _ = time.Parse(time.RFC3339, "2026-01-01T00:00:00Z")

var sharedPoolLabelMatcherPattern = regexp.MustCompile(`(^|[,{])\s*sharedpool_id\s*=\s*"([^"]+)"\s*([,}])`)

const (
	MetricScopeQueryParam = "metrics_scope"
	// SharedResourceDashboardUID scopes cross-account routing to the dedicated
	// Shared Resource dashboard. The optional variable supports future dashboard
	// revisions without accepting any account identifiers from the browser.
	SharedResourceDashboardUID       = "5592da408d0ffe87nextgenv9_0_x"
	SharedResourceDashboardUIDHeader = "X-Dashboard-Uid"
	SharedResourceScopeParam         = "var-shared_resource_scope"
	SharedResourceScopeValue         = "resource_pool"

	MetricScopeChild  = string(o11yurls.MetricsScopeChild)
	MetricScopeParent = string(o11yurls.MetricsScopeParent)
)

// TiDBClusterMetricHandler check which metrics backend to proxy. <vm|thanos|o11y>
type TiDBClusterMetricHandler struct {
	rbac       rbac2.Auth
	o11yEnable bool
	// todo(shenjun): there is a better way to encapsulate the client and config
	o11yCli *o11y.Client
	// metricSelectAddr points to vmobjselect endpoint
	metricSelectAddr    *config.MetricSelectAddressConfig
	env                 string
	resourcePoolMetrics *resourcePoolMetricsTargetResolver
}

func NewTiDBClusterMetricHandler(rbac rbac2.Auth, metricSelectAddr *config.MetricSelectAddressConfig, env string) *TiDBClusterMetricHandler {
	return &TiDBClusterMetricHandler{rbac: rbac, metricSelectAddr: metricSelectAddr, env: env}
}

func (handler *TiDBClusterMetricHandler) SetO11y(o11yCli *o11y.Client) {
	handler.o11yCli = o11yCli
	handler.o11yEnable = true
	handler.resourcePoolMetrics = newResourcePoolMetricsTargetResolver(db.GlobalDB(), o11yCli)
}

func (handler *TiDBClusterMetricHandler) GinHandle(
	c *gin.Context,
	getOrgType func(c *gin.Context) string,
	getOrgID func(c *gin.Context) uint,
	getClusterIDType func(c *gin.Context) string,
	getControlPlaneInfo func(c *gin.Context) string,
	routerPathFunc func(string) string,
) {
	user := session.GetUser(c)
	if user == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}

	orgType := getOrgType(c)
	ctx := c.Request.Context()
	if log.GeTraceId(ctx) == "" {
		log.Info(ctx, "metric-proxy GinHandle  the trace id is empty, start a new trace id")
		// Fallback for entrypoints without tracing middleware.
		ctx = log.StartTrace("metric-proxy", "GinHandle")
		c.Request = c.Request.WithContext(ctx)
		log.Info(ctx, "metric-proxy GinHandle  new trace id")
	}
	if isSharedResourceDashboardRequest(c.Request) {
		ctx = contextWithSharedResourceDashboard(ctx)
		c.Request = c.Request.WithContext(ctx)
	}

	metricsScope, err := getMetricsScope(c)
	if err != nil {
		_ = c.Error(err)
		return
	}

	controlPlaneInfo := ""
	if getControlPlaneInfo != nil {
		controlPlaneInfo = getControlPlaneInfo(c)
	}
	if controlPlaneInfo == "" {
		controlPlaneInfo = GetPromQLData(GetO11yPromQL(c), ControlPlaneInfoVariable)
	}

	oid := getOrgID(c)
	clusterID := getClusterIDType(c)
	clusterID = normalizeClusterID(clusterID)
	if requestQueriesSharedPool(c.Request, clusterID) {
		ctx = contextWithSharedResourceDashboard(ctx)
		c.Request = c.Request.WithContext(ctx)
	}
	ctx = contextWithClusterID(ctx, clusterID)
	c.Request = c.Request.WithContext(ctx)

	if metricsScope == MetricScopeParent && controlPlaneInfo != "" {
		hasClusterContext := oid > 0 && clusterID != ""
		if !hasClusterContext && !user.Internal {
			_ = c.Error(error2.ErrForbidden.New("permission deny"))
			return
		}
		if hasClusterContext && orgType == "cloud" && !user.Internal {
			ok, err := common.CheckExternalUserPermission(user, oid, clusterID)
			if err != nil || !ok {
				log.Error(ctx, "external user permission denied", zap.Error(err), zap.Bool("has permission", ok))
				_ = c.Error(error2.ErrForbidden.New("permission deny"))
				return
			}
		}

		targetScheme, targetHost, requestModifyFunc, respModifyFunc, err := handler.handleControlPlaneParentMetrics(ctx, controlPlaneInfo, routerPathFunc) //nolint:bodyclose
		if err == nil {
			proxyModifyResponse(ctx, c, targetScheme, targetHost, requestModifyFunc, respModifyFunc)
			return
		}
		log.Warn(ctx, "resolve control plane parent metrics target failed",
			zap.String("controlPlaneInfo", controlPlaneInfo),
			zap.Bool("hasClusterContext", hasClusterContext),
			zap.Error(err))
		if !errors.Is(err, o11y.ErrNotFound) {
			_ = c.Error(error2.ErrInternal.New("resolve metrics target failed: %s", err.Error()))
			return
		}
		if !hasClusterContext {
			_ = c.Error(error2.ErrNotFound.New("o11y urls metrics target resolve failed: no o11y cluster found"))
			return
		}
	}

	if oid <= 0 || clusterID == "" {
		if oid <= 0 {
			_ = c.Error(error2.ErrNotFound.New("not found the organization"))
			return
		}
		_ = c.Error(error2.ErrNotFound.New("cluster is not found"))
		return
	}

	// check external user permission
	if orgType == "cloud" && !user.Internal {
		ok, err := common.CheckExternalUserPermission(user, oid, clusterID)
		if err != nil || !ok {
			log.Error(ctx, "external user permission denied", zap.Error(err), zap.Bool("has permission", ok))
			_ = c.Error(error2.ErrForbidden.New("permission deny"))
			return
		}
	}

	handler.proxy(ctx, c, orgType, oid, clusterID, user, metricsScope, controlPlaneInfo, routerPathFunc)
}

func isLogicalControlPlaneMetricsInfo(controlPlaneInfo string) bool {
	parts := strings.Split(controlPlaneInfo, "/")
	return len(parts) >= 2 && parts[0] == o11y.SeedID
}

func (handler *TiDBClusterMetricHandler) proxy(
	ctx context.Context,
	c *gin.Context,
	orgType string,
	oid uint,
	clusterID string,
	user *session.CtxUser,
	metricsScope string,
	controlPlaneInfo string,
	routerPathFunc func(string2 string) string,
) {
	targetScheme, targetHost, requestModifyFunc, respModifyFunc, err := handler.handle(ctx, orgType, oid, clusterID, user, metricsScope, controlPlaneInfo, routerPathFunc) //nolint:bodyclose
	if err != nil {
		_ = c.Error(err)
		return
	}
	proxyModifyResponse(ctx, c, targetScheme, targetHost, requestModifyFunc, respModifyFunc)
}

func (handler *TiDBClusterMetricHandler) handleControlPlaneParentMetrics(
	ctx context.Context,
	controlPlaneInfo string,
	routerPathFunc func(string2 string) string,
) (
	string,
	string,
	func(r *http.Request) *http.Request,
	func(resp *http.Response) error,
	error,
) {
	if !handler.o11yEnable {
		return "", "", nil, nil, errors.New("o11y is not enabled")
	}

	resolver := o11yurls.NewClusterURLsResolver(handler.o11yCli)
	var (
		target *o11yurls.MetricsTarget
		err    error
	)
	if isLogicalControlPlaneMetricsInfo(controlPlaneInfo) {
		target, err = resolver.ResolveLogicalControlPlaneMetricsTarget(ctx, controlPlaneInfo)
	} else {
		target, err = resolver.ResolveServerlessControlPlaneMetricsTarget(ctx, controlPlaneInfo)
	}
	if err != nil {
		return "", "", nil, nil, err
	}
	return buildProxyForMetricsTarget(ctx, target, nil, routerPathFunc) //nolint:bodyclose
}

func (handler *TiDBClusterMetricHandler) proxyLogicalControlPlaneMetrics(
	ctx context.Context,
	c *gin.Context,
	user *session.CtxUser,
	controlPlaneInfo string,
	routerPathFunc func(string2 string) string,
) {
	if !user.Internal {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}
	if !handler.o11yEnable {
		_ = c.Error(errors.New("o11y is not enabled"))
		return
	}

	target, err := o11yurls.NewClusterURLsResolver(handler.o11yCli).ResolveLogicalControlPlaneMetricsTarget(ctx, controlPlaneInfo)
	if err != nil {
		log.Error(ctx, "resolve logical control plane metrics target failed",
			zap.String("controlPlaneInfo", controlPlaneInfo),
			zap.Error(err))
		if errors.Is(err, o11y.ErrNotFound) {
			_ = c.Error(error2.ErrNotFound.New("o11y urls metrics target resolve failed: no o11y cluster found"))
			return
		}
		_ = c.Error(error2.ErrInternal.New("resolve metrics target failed: %s", err.Error()))
		return
	}

	targetScheme, targetHost, requestModifyFunc, respModifyFunc, err := buildProxyForMetricsTarget(ctx, target, nil, routerPathFunc) //nolint:bodyclose
	if err != nil {
		_ = c.Error(err)
		return
	}
	proxyModifyResponse(ctx, c, targetScheme, targetHost, requestModifyFunc, respModifyFunc)
}

func (handler *TiDBClusterMetricHandler) handleForOP(
	ctx context.Context,
	oid uint,
	user *session.CtxUser,
	routerPathFunc func(string2 string) string,
) (
	string,
	string,
	func(r *http.Request) *http.Request,
	func(resp *http.Response) error,
	error,
) {
	// check external user permission
	ok, err := handler.rbac.Auth(user.UserID, user.Internal, rbac2.Cluster(), rbac2.ReadActing(), oid)
	if err != nil || !ok {
		return "", "", nil, nil, error2.ErrForbidden.New("permission deny")
	}

	targetHost := handler.metricSelectAddr.OpAddress.Authority()
	requestModifyFunc := buildModifyFuncForVM(&handler.metricSelectAddr.OpAddress, oid, routerPathFunc)
	respModifyFunc := func(resp *http.Response) error {
		resp.Header.Set("X-Metrics-From", "victoriametrics")
		return nil
	}
	return "http", targetHost, requestModifyFunc, respModifyFunc, err
}

func (handler *TiDBClusterMetricHandler) handle(
	ctx context.Context,
	orgType string,
	oid uint,
	clusterID string,
	user *session.CtxUser,
	metricsScope string,
	controlPlaneInfo string,
	routerPathFunc func(string2 string) string,
) (
	string,
	string,
	func(r *http.Request) *http.Request,
	func(resp *http.Response) error,
	error,
) {
	var err error
	if orgType != "cloud" {
		return handler.handleForOP(ctx, oid, user, routerPathFunc) //nolint:bodyclose
	}

	if !user.Internal {
		// check external user permission
		ok, err := common.CheckExternalUserPermission(user, oid, clusterID)
		if err != nil || !ok {
			return "", "", nil, nil, error2.ErrForbidden.New("permission deny")
		}
	}
	if !handler.o11yEnable {
		return "", "", nil, nil, errors.New("o11y is not enabled")
	}

	org, err := datasource.GetOrganization(user, oid)
	if err != nil {
		log.Error(ctx, "get org from datasource failed", zap.Error(err))
		return "", "", nil, nil, err
	}
	cluster, err := datasource.GetCloudCluster(user, clusterID, oid)
	if err != nil {
		log.Error(ctx, "get cluster from datasource failed", zap.Error(err))
		return "", "", nil, nil, errors.New("get cluster from datasource failed")
	}
	cluster.Organization = *org
	log.Info(ctx, "get cluster from datasource success", zap.Any("cluster", cluster))
	if metricsScope == MetricScopeParent {
		if !slices.Contains(o11y.SharedTypes, cluster.DeployTypeV2) {
			return "", "", nil, nil, error2.ErrBadRequest.New("parent metrics scope is only supported for starter/essential clusters")
		}
		if controlPlaneInfo == "" {
			return "", "", nil, nil, error2.ErrBadRequest.New("control_plane_info is required for parent metrics scope")
		}
	}

	target, err := o11yurls.NewClusterURLsResolver(handler.o11yCli).ResolveMetricsTarget(
		ctx,
		cluster,
		o11yurls.MetricsScope(metricsScope),
		controlPlaneInfo,
	)
	if err != nil {
		log.Error(ctx, "resolve metrics target failed",
			zap.String("clusterID", cluster.ClusterID),
			zap.String("metricsScope", metricsScope),
			zap.String("deployType", cluster.DeployType),
			zap.String("deployTypeV2", cluster.DeployTypeV2),
			zap.String("provider", cluster.Provider),
			zap.String("region", cluster.Region),
			zap.Error(err))
		if errors.Is(err, o11y.ErrNotFound) {
			return "", "", nil, nil, error2.ErrNotFound.New("o11y urls metrics target resolve failed: no o11y cluster found")
		}
		return "", "", nil, nil, error2.ErrInternal.New("resolve metrics target failed: %s", err.Error())
	}
	if metricsScope == MetricScopeChild &&
		isSharedResourceDashboardContext(ctx) &&
		cluster.DeployTypeV2 == o11y.DeployTypeV2EssentialV2ResourcePool {
		target, err = handler.resourcePoolMetrics.resolve(ctx, cluster, target)
		if err != nil {
			log.Error(ctx, "resolve shared resource multi-account metrics target failed",
				zap.String("resourcePoolID", cluster.ClusterID),
				zap.Error(err))
			return "", "", nil, nil, error2.ErrInternal.New("resolve shared resource metrics target failed: %s", err.Error())
		}
	}
	return buildProxyForMetricsTarget(ctx, target, cluster, routerPathFunc) //nolint:bodyclose
}

func isSharedResourceDashboardRequest(r *http.Request) bool {
	if r == nil {
		return false
	}
	// Accept an explicitly forwarded dashboard UID when the Grafana proxy or
	// datasource provides one. Current Clinic links primarily use the scope
	// marker below, with the Referer UID retained for backward compatibility.
	if r.Header.Get(SharedResourceDashboardUIDHeader) == SharedResourceDashboardUID {
		return true
	}
	referer, err := url.Parse(r.Referer())
	if err == nil && referer != nil {
		if referer.Query().Get(SharedResourceScopeParam) == SharedResourceScopeValue {
			return true
		}
		pathParts := strings.Split(strings.Trim(referer.Path, "/"), "/")
		for i := 0; i+2 < len(pathParts); i++ {
			if pathParts[i] == "grafana" && pathParts[i+1] == "d" && pathParts[i+2] == SharedResourceDashboardUID {
				return true
			}
		}
	}
	return r.URL.Query().Get(SharedResourceScopeParam) == SharedResourceScopeValue
}

func requestQueriesSharedPool(r *http.Request, resourcePoolID string) bool {
	if r == nil || resourcePoolID == "" {
		return false
	}
	query := r.URL.Query()
	for _, key := range []string{"query", "match[]", "match"} {
		for _, expression := range query[key] {
			for _, match := range sharedPoolLabelMatcherPattern.FindAllStringSubmatch(expression, -1) {
				if len(match) > 2 && match[2] == resourcePoolID {
					return true
				}
			}
		}
	}
	return false
}

func getMetricsScope(c *gin.Context) (string, error) {
	scope := c.Query(MetricScopeQueryParam)
	if scope == "" {
		return MetricScopeChild, nil
	}
	switch scope {
	case MetricScopeChild, MetricScopeParent:
		return scope, nil
	default:
		return "", error2.ErrBadRequest.New("invalid metrics_scope")
	}
}

func buildProxyForMetricsTarget(
	ctx context.Context,
	target *o11yurls.MetricsTarget,
	cluster *db.Cluster,
	routerPathFunc func(string2 string) string,
) (
	string,
	string,
	func(r *http.Request) *http.Request,
	func(resp *http.Response) error,
	error,
) {
	if target == nil || target.MetricsURL == "" {
		return "", "", nil, nil, error2.ErrInternal.New("metrics target is empty")
	}
	metricsURL, err := url.Parse(target.MetricsURL)
	if err != nil || metricsURL.Scheme == "" || metricsURL.Host == "" {
		log.Error(ctx, "parse metrics target URL failed", zap.Error(err), zap.String("url", target.MetricsURL))
		return "", "", nil, nil, error2.ErrInternal.New("parse metrics target URL failed")
	}
	targetScheme := metricsURL.Scheme
	targetHost := metricsURL.Host
	targetQuery := metricsURL.Query()
	requestModifyFunc := func(r *http.Request) *http.Request {
		r.URL.Path = joinO11yInternalReadPath(metricsURL.Path, routerPathFunc(r.URL.Path))
		r.URL.RawPath = joinO11yInternalReadPath(metricsURL.Path, routerPathFunc(r.URL.RawPath))
		mergeMetricsTargetQueryParams(r, targetQuery)
		stripMetricsScopeParam(r)
		log.Info(r.Context(), "forward to resolved metrics target",
			zap.String("targetHost", targetHost),
			zap.String("path", r.URL.Path),
			zap.String("url", r.URL.String()))
		r.Host = targetHost
		return r
	}
	if cluster != nil && target.O11yCluster != nil && isByocDeployType(cluster.DeployType) {
		requestModifyFunc = withByocRequestHeaders(requestModifyFunc, cluster, target.O11yCluster)
	} else if target.O11yCluster != nil {
		requestModifyFunc = withO11yClusterRequestHeaders(requestModifyFunc, target.O11yCluster)
	}
	respModifyFunc := func(resp *http.Response) error {
		resp.Header.Set("X-Metrics-From", "o11y")
		if resp.StatusCode != http.StatusOK {
			log.Error(resp.Request.Context(), "forward to resolved metrics target failed",
				zap.String("url", resp.Request.URL.String()),
				zap.Int("status", resp.StatusCode))
		}
		return nil
	}
	return targetScheme, targetHost, requestModifyFunc, respModifyFunc, nil
}

func mergeMetricsTargetQueryParams(r *http.Request, targetQuery url.Values) {
	if len(targetQuery) == 0 {
		return
	}
	q := r.URL.Query()
	for key, values := range targetQuery {
		q.Del(key)
		for _, value := range values {
			q.Add(key, value)
		}
	}
	r.URL.RawQuery = q.Encode()
}

func stripMetricsScopeParam(r *http.Request) {
	q := r.URL.Query()
	if _, ok := q[MetricScopeQueryParam]; !ok {
		return
	}
	q.Del(MetricScopeQueryParam)
	r.URL.RawQuery = q.Encode()
}

func isByocDeployType(deployType string) bool {
	return deployType == o11y.ByocType || deployType == o11y.ByocHostType
}

func withByocRequestHeaders(
	requestModifyFunc func(r *http.Request) *http.Request,
	cluster *db.Cluster,
	o11yCluster *o11y.ClusterInfo,
) func(r *http.Request) *http.Request {
	return withByocHeaders(
		requestModifyFunc,
		o11yCluster.BizType,
		fmt.Sprintf("%d", cluster.TenantID),
		cluster.Provider,
		cluster.Region,
	)
}

func withByocHeaders(
	requestModifyFunc func(r *http.Request) *http.Request,
	bizType string,
	tenantID string,
	provider string,
	region string,
) func(r *http.Request) *http.Request {
	return func(r *http.Request) *http.Request {
		r = requestModifyFunc(r)
		r.Header.Set("X-Biz-Type", bizType)
		r.Header.Set("X-Org-Id", tenantID)
		r.Header.Set("X-Region-Id", fmt.Sprintf("%s-%s", provider, region))
		return r
	}
}

func (handler *TiDBClusterMetricHandler) handleForPremium(
	ctx context.Context,
	cluster *db.Cluster,
	org *db.Organization,
	routerPathFunc func(string2 string) string,
) (
	targetScheme string,
	targetHost string,
	reqModifyFunc func(r *http.Request) *http.Request,
	respModifyFunc func(resp *http.Response) error,
	err error,
) {
	if !IsUsWest2HistoryPremiumCluster(cluster.ClusterID) && !IsUsWest2HistoryNGHostCluster(cluster.ClusterID) {
		region := cluster.Region
		if cluster.Provider == "aws" {
			if region == "ap-south-1" {
				// ap-south-1 keeps the raw region without any premium/ng suffix.
			} else if region == "ap-southeast-2" {
				region += "-ng"
			} else {
				region += "-premium"
			}
		}
		targetHost = handler.o11yCli.GenDataServiceProxyHost(region, cluster.Provider)
		reqModifyFunc = func(r *http.Request) *http.Request {
			stripMetricsScopeParam(r)
			ep := strings.TrimPrefix(routerPathFunc(r.URL.Path), "/")
			epRaw := strings.TrimPrefix(routerPathFunc(r.URL.RawPath), "/")
			r.URL.Path = path.Join("/internal/v2/metrics/multitenant", ep)
			r.URL.RawPath = path.Join("/internal/v2/metrics/multitenant", epRaw)
			log.Info(r.Context(), "forward to o11y",
				zap.String("targetHost", targetHost),
				zap.String("path", r.URL.Path),
				zap.String("url", r.URL.String()))
			r.Host = targetHost
			return r
		}
	} else {
		// non-migrated historical clusters
		var param *o11y.ListClusterParam
		if cluster.DeployType == o11y.PremiumType {
			param = handler.o11yCli.GetNextGenTidbClusterRequest(
				cluster.Provider,
				cluster.Region,
				fmt.Sprintf("%d", org.DbaasOrgID),
				cluster.ClusterID,
			)
		} else { // nextgen-host
			param = handler.o11yCli.GetNextGenHostTidbClusterRequest(
				cluster.Provider,
				cluster.Region,
				fmt.Sprintf("%d", org.DbaasOrgID),
				cluster.ClusterID,
			)
		}
		targetHost = handler.o11yCli.GenDataServiceProxyHost(cluster.Region, cluster.Provider)
		reqModifyFunc = func(r *http.Request) *http.Request {
			stripMetricsScopeParam(r)
			r.URL.Path = handler.o11yCli.GenHistoricalDataProxyPath(routerPathFunc(r.URL.Path), param.BizType, param.TenantID, param.ProjectID, param.ApplicationID)
			r.URL.RawPath = handler.o11yCli.GenHistoricalDataProxyPath(routerPathFunc(r.URL.RawPath), param.BizType, param.TenantID, param.ProjectID, param.ApplicationID)
			log.Info(r.Context(), "forward to o11y historical data service",
				zap.String("path", r.URL.Path),
				zap.String("targetHost", targetHost))
			r.Host = targetHost
			return r
		}
	}
	respModifyFunc = func(resp *http.Response) error {
		resp.Header.Set("X-Metrics-From", "o11y")
		if resp.StatusCode != http.StatusOK {
			log.Warn(resp.Request.Context(), "forward to o11y failed",
				zap.String("path", resp.Request.URL.Path),
				zap.String("url", resp.Request.URL.String()),
				zap.Int("status", resp.StatusCode))
		}
		return nil
	}
	return "https", targetHost, reqModifyFunc, respModifyFunc, nil
}

func buildModifyFuncForVM(addr *config.VMClusterAddressConfig, oid uint, routerPath func(string2 string) string) func(r *http.Request) *http.Request {
	return func(r *http.Request) *http.Request {
		stripMetricsScopeParam(r)
		if addr.InVMCluster() {
			r.URL.Scheme = "http"
			r.URL.Path = fmt.Sprintf("/select/%d/prometheus%s", oid, routerPath(r.URL.Path))
			r.URL.RawPath = fmt.Sprintf("/select/%d/prometheus%s", oid, routerPath(r.URL.RawPath))
		} else {
			r.URL.Path = routerPath(r.URL.Path)
			r.URL.RawPath = routerPath(r.URL.RawPath)
		}
		return r
	}
}

func buildModifyFuncByO11yID(
	cli *o11y.Client,
	dataServiceHost, o11yClusterID string,
	routerPath func(string) string,
	o11yPathV2 bool,
) func(r *http.Request) *http.Request {
	return func(r *http.Request) *http.Request {
		stripMetricsScopeParam(r)
		if o11yPathV2 {
			r.URL.Path = cli.GenDataServiceProxyPathV2(o11yClusterID, routerPath(r.URL.Path))
			r.URL.RawPath = cli.GenDataServiceProxyPathV2(o11yClusterID, routerPath(r.URL.RawPath))
		} else {
			r.URL.Path = cli.GenDataServiceProxyPath(o11yClusterID, routerPath(r.URL.Path))
			r.URL.RawPath = cli.GenDataServiceProxyPath(o11yClusterID, routerPath(r.URL.RawPath))
		}
		log.Info(r.Context(), "forward to o11y",
			zap.String("host", dataServiceHost),
			zap.String("path", r.URL.Path),
			zap.String("url", r.URL.String()))
		r.Host = dataServiceHost
		return r
	}
}

func proxyModifyResponse(
	ctx context.Context,
	c *gin.Context,
	scheme, host string,
	reqModify func(r *http.Request) *http.Request,
	respModify func(r *http.Response) error,
) {
	url := &url2.URL{
		Scheme: scheme,
		Host:   host,
	}
	// wlt
	// log.Info(ctx, "proxyModifyResponse", zap.String("url", url.String()))
	proxy := httputil.NewSingleHostReverseProxy(url)
	originDirector := proxy.Director
	director := func(req *http.Request) {
		req = reqModify(req)
		originDirector(req)
		log.Info(req.Context(), "proxy request headers before dispatch",
			zap.String("cluster_id", clusterIDFromContext(req.Context())),
			zap.Bool("shared_resource_dashboard", isSharedResourceDashboardContext(req.Context())),
			zap.String("x_dashboard_uid", req.Header.Get(SharedResourceDashboardUIDHeader)),
			zap.String("method", req.Method),
			zap.String("url", req.URL.String()),
			zap.String("x_biz_type", req.Header.Get("X-Biz-Type")),
			zap.String("x_org_id", req.Header.Get("X-Org-Id")),
			zap.String("x_region_id", req.Header.Get("X-Region-Id")))
	}
	proxy.Director = director
	proxy.ModifyResponse = respModify
	proxy.ErrorHandler = func(_ http.ResponseWriter, request *http.Request, err error) {
		log.Error(request.Context(), "proxy failed", zap.Error(err), zap.String("host", request.URL.Host), zap.String("path", request.URL.Path))
	}
	proxy.ServeHTTP(c.Writer, c.Request)
}
