package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"maps"
	"net"
	"net/http"
	"net/url"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"time"
	"unicode"

	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/config"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/dto"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"
	clinictracing "github.com/pingcap-inc/clinic/clinic_data_service/pkg/tracing"
	"github.com/prometheus/prometheus/model/labels"
	promparser "github.com/prometheus/prometheus/promql/parser"
	"go.uber.org/zap"
)

var (
	errCrossMetricSource        = errors.New("unsupported cross-source metrics query")
	errMetricLabelsNoSeries     = errors.New("metric labels no series")
	errInvalidBYOCMetricsTarget = errors.New("invalid BYOC metrics routing")
)

// MetricsQueryService handles metrics queries via clinic-internal cluster-dataurls API
type MetricsQueryService struct {
	clinicClient     *clinic_internal_api.Client
	httpClient       *http.Client
	dataPlaneMetrics map[string]bool // allowlist: metrics that should use DataPlane endpoint
}

// NewMetricsQueryService creates a new MetricsQueryService from DataProxyConfig
func NewMetricsQueryService(cfg *config.DataProxyConfig) *MetricsQueryService {
	// Load the DataPlane routing allowlist from DataProxyConfig.
	dataPlaneMetrics := make(map[string]bool)
	for _, m := range cfg.MetricsQuery.MetricsMapping.DataPlaneMetrics {
		dataPlaneMetrics[m] = true
	}

	return &MetricsQueryService{
		clinicClient: clinic_internal_api.NewClientWithTimeout(cfg.ClinicAPIBase, 2*time.Minute),
		httpClient: &http.Client{
			Timeout:   2 * time.Minute,
			Transport: clinictracing.NewHTTPTransport(nil),
		},
		dataPlaneMetrics: dataPlaneMetrics,
	}
}

// GetMetricCategory determines which endpoint to use for a metric
func (s *MetricsQueryService) GetMetricCategory(query string) dto.MetricCategory {
	category, err := s.classifyMetricCategory(query)
	if err != nil {
		return dto.MetricCategoryCluster
	}
	return category
}

func (s *MetricsQueryService) classifyMetricCategory(queries ...string) (dto.MetricCategory, error) {
	var dataPlaneMetrics, clusterMetrics []string
	seenDataPlane := make(map[string]struct{})
	seenCluster := make(map[string]struct{})

	for _, query := range queries {
		metricNames := extractMetricNames(query)
		if len(metricNames) == 0 {
			// Keep the old conservative fallback for invalid or partial PromQL.
			metricNames = []string{extractMetricName(query)}
		}

		for _, metricName := range metricNames {
			if metricName == "" {
				continue
			}
			if s.isDataPlaneMetric(metricName) {
				if _, ok := seenDataPlane[metricName]; !ok {
					seenDataPlane[metricName] = struct{}{}
					dataPlaneMetrics = append(dataPlaneMetrics, metricName)
				}
				continue
			}
			if _, ok := seenCluster[metricName]; !ok {
				seenCluster[metricName] = struct{}{}
				clusterMetrics = append(clusterMetrics, metricName)
			}
		}
	}

	if len(dataPlaneMetrics) > 0 && len(clusterMetrics) > 0 {
		slices.Sort(dataPlaneMetrics)
		slices.Sort(clusterMetrics)
		return dto.MetricCategoryCluster, fmt.Errorf("%w: Clinic Data Proxy cannot execute one PromQL expression across both data-plane and cluster metrics; data-plane metrics [%s], cluster metrics [%s]; split the query into separate requests",
			errCrossMetricSource,
			strings.Join(dataPlaneMetrics, ", "),
			strings.Join(clusterMetrics, ", "))
	}
	if len(dataPlaneMetrics) > 0 {
		return dto.MetricCategoryDataPlane, nil
	}
	return dto.MetricCategoryCluster, nil
}

func (s *MetricsQueryService) isDataPlaneMetric(metricName string) bool {
	if metricName == "" {
		return false
	}
	if _, ok := s.dataPlaneMetrics[metricName]; ok {
		return true
	}
	for pattern := range s.dataPlaneMetrics {
		if pattern == metricName || !strings.ContainsAny(pattern, "*?") {
			continue
		}
		if matched, err := filepath.Match(pattern, metricName); err == nil && matched {
			return true
		}
	}
	return false
}

func extractMetricNames(query string) []string {
	expr, err := promparser.ParseExpr(query)
	if err != nil {
		return nil
	}
	seen := make(map[string]struct{})
	var metricNames []string
	promparser.Inspect(expr, func(node promparser.Node, _ []promparser.Node) error {
		if vs, ok := node.(*promparser.VectorSelector); ok {
			metricName := vectorSelectorMetricName(vs)
			if metricName == "" {
				return nil
			}
			if _, ok := seen[metricName]; ok {
				return nil
			}
			seen[metricName] = struct{}{}
			metricNames = append(metricNames, metricName)
		}
		return nil
	})
	return metricNames
}

// extractMetricName extracts the base metric name from a PromQL query
func extractMetricName(query string) string {
	// Simple extraction: get the first identifier before any special character
	query = strings.TrimSpace(query)

	// Skip common functions like rate(, sum(, avg(, etc.
	for {
		idx := strings.Index(query, "(")
		if idx == -1 {
			break
		}
		// Check if it's a function call
		prefix := strings.TrimSpace(query[:idx])
		if isPromQLFunction(prefix) {
			query = query[idx+1:]
		} else {
			break
		}
	}

	// Find the metric name (ends at { or [ or space or end)
	for i, c := range query {
		if c == '{' || c == '[' || c == ' ' || c == '(' || c == ')' {
			return query[:i]
		}
	}
	return query
}

// isPromQLFunction checks if a string is a common PromQL function
func isPromQLFunction(s string) bool {
	functions := map[string]bool{
		"rate": true, "irate": true, "increase": true,
		"sum": true, "avg": true, "min": true, "max": true, "count": true,
		"sum_over_time": true, "avg_over_time": true, "min_over_time": true, "max_over_time": true,
		"histogram_quantile": true, "topk": true, "bottomk": true,
		"abs": true, "ceil": true, "floor": true, "round": true,
		"delta": true, "deriv": true, "predict_linear": true,
		"label_replace": true, "label_join": true,
		"vector": true, "scalar": true, "time": true,
		"by": true, "without": true, "on": true, "ignoring": true,
	}
	return functions[strings.ToLower(s)]
}

// deployTypeV2 values for metrics label injection
const (
	deployTypeV2Dedicated                   = "dedicated"
	deployTypeV2Starter                     = "starter"
	deployTypeV2Essential                   = "essential"
	deployTypeV2Premium                     = "premium"
	deployTypeV2PremiumReserved             = "premium reserved"
	deployTypeV2EssentialV2                 = "essential v2"
	deployTypeV2PremiumResourcePool         = "premium resource pool"
	deployTypeV2PremiumReservedResourcePool = "premium reserved resource pool"
	deployTypeV2EssentialV2ResourcePool     = "essential v2 resource pool"
	deployTypeV2ByocResourcePool            = "byoc resource pool"
	deployTypeV2Byoc                        = "byoc"

	labelKeyspaceID       = "keyspace_id"
	labelO11yCluster      = "cluster"
	labelInstance         = "instance"
	labelName             = "name"
	labelClusterID        = "cluster_id"
	labelTidbClusterID    = "tidb_cluster_id"
	labelControlPlaneInfo = "control_plane_info"

	metricsTidbServerConnections = "tidb_server_connections"
)

// isStarterOrEssential returns true if deployTypeV2 is Starter or Essential (shared tier)
func isStarterOrEssential(d string) bool {
	d = strings.ToLower(strings.TrimSpace(d))
	return d == deployTypeV2Starter || d == deployTypeV2Essential
}

// isPremiumOrByoc returns true if deployTypeV2 is Premium or Byoc (nextgen tier)
func isPremiumOrByoc(d string) bool {
	d = strings.ToLower(strings.TrimSpace(d))
	return d == deployTypeV2Premium || d == deployTypeV2PremiumReserved || d == deployTypeV2EssentialV2 || d == deployTypeV2Byoc
}

func isPremiumOrByocResourcePool(d string) bool {
	d = strings.ToLower(strings.TrimSpace(d))
	return d == deployTypeV2PremiumResourcePool || d == deployTypeV2PremiumReservedResourcePool ||
		d == deployTypeV2EssentialV2ResourcePool || d == deployTypeV2ByocResourcePool
}

// MetricLabel is the label name and value to inject for a metric.
type MetricLabel struct {
	Name      string
	MatchType labels.MatchType
	Value     string
}

// MetricLabelMap maps metric name -> label to inject. Used for Starter/Essential where different metrics may need different label name/value.
type MetricLabelMap map[string]MetricLabel

// injectLabelIntoPromQL injects labels into every vector selector using the official parser.
// For each VectorSelector, the label to inject is taken from perMetric[metricName] if present, otherwise from defaultLabel.
// If the chosen label has empty Name or Value, that selector is skipped. If parsing fails, the original query is returned unchanged.
func injectLabelIntoPromQL(query string, defaultLabel MetricLabel, perMetric MetricLabelMap) string {
	expr, err := promparser.ParseExpr(query)
	if err != nil {
		return query
	}
	promparser.Inspect(expr, func(node promparser.Node, _ []promparser.Node) error {
		if vs, ok := node.(*promparser.VectorSelector); ok {
			metricName := vectorSelectorMetricName(vs)

			label := defaultLabel
			if perMetric != nil {
				if m, ok := perMetric[metricName]; ok {
					label = m
				} else {
					for key, val := range perMetric {
						if matched, err := filepath.Match(key, metricName); err == nil && matched {
							label = val
						}
					}
				}

			}
			if label.Name == "" || label.Value == "" {
				return nil
			}
			for _, m := range vs.LabelMatchers {
				if m.Name == label.Name {
					return nil
				}
			}
			vs.LabelMatchers = append(vs.LabelMatchers, labels.MustNewMatcher(label.MatchType, label.Name, label.Value))
		}
		return nil
	})
	return expr.String()
}

// vectorSelectorMetricName returns the metric name for a VectorSelector (vs.Name or __name__ from matchers).
func vectorSelectorMetricName(vs *promparser.VectorSelector) string {
	if vs.Name != "" {
		return vs.Name
	}
	for _, m := range vs.LabelMatchers {
		if m.Name == "__name__" {
			return m.Value
		}
	}
	return ""
}

// prometheusSeriesResult parses Prometheus /api/v1/series response: {"status":"success","data":[{"__name__":"...", "label":"value", ...}, ...]}
type prometheusSeriesResult struct {
	Data []map[string]string `json:"data"`
}

// fetchKeyspaceIDDefaultRange is the default range when no start/end is provided (query last 1 day).
const fetchKeyspaceIDDefaultRange = 24 * time.Hour

// fetchMetricLabels queries tidb_server_connections by cluster_id via the series API and returns the label values from the first series.
// If start and end are both zero, the last 1 day is queried. Otherwise the given range is used.
func (s *MetricsQueryService) fetchMetricLabels(ctx context.Context, clusterMetricsURL, clusterID string, start, end time.Time) (string, string, string, error) {
	baseURL := normalizePrometheusURL(clusterMetricsURL)
	queryURL := strings.TrimRight(baseURL, "/") + "/series"
	match := fmt.Sprintf(`%s{cluster_id="%s"}`, metricsTidbServerConnections, clusterID)

	if start.IsZero() && end.IsZero() {
		end = time.Now()
		start = end.Add(-fetchKeyspaceIDDefaultRange)
	}
	if end.Before(start) || end.Equal(start) {
		end = start.Add(fetchKeyspaceIDDefaultRange)
	}
	params := url.Values{}
	params.Add("match[]", match)
	params.Set("start", fmt.Sprintf("%d", start.Unix()))
	params.Set("end", fmt.Sprintf("%d", end.Unix()))
	u, err := url.Parse(queryURL)
	if err != nil {
		return "", "", "", fmt.Errorf("parse query URL: %w", err)
	}
	u.RawQuery = params.Encode()
	fullURL := u.String()
	log.Info(ctx, "fetching keyspace_id from tidb_server_connections (series)",
		zap.String("clusterID", clusterID),
		zap.Time("start", start),
		zap.Time("end", end))
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
	if err != nil {
		return "", "", "", fmt.Errorf("create request: %w", err)
	}
	resp, err := s.httpClient.Do(req)
	if err != nil {
		return "", "", "", fmt.Errorf("request tidb_server_connections: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", "", "", fmt.Errorf("read response: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		log.Error(ctx, "tidb_server_connections returned status not 200",
			zap.String("clusterID", clusterID),
			zap.Int("statusCode", resp.StatusCode),
			zap.Int("bodyLength", len(body)))
		return "", "", "", fmt.Errorf("tidb_server_connections returned status %d: %s", resp.StatusCode, string(body))
	}
	var result prometheusSeriesResult
	if err := json.Unmarshal(body, &result); err != nil {
		log.Error(ctx, "failed to parse response",
			zap.String("clusterID", clusterID),
			zap.Int("bodyLength", len(body)),
			zap.Error(err))
		return "", "", "", fmt.Errorf("parse response: %w", err)
	}
	if len(result.Data) == 0 {
		log.Warn(ctx, "tidb_server_connections returned no series for cluster_id",
			zap.String("clusterID", clusterID),
			zap.Int("bodyLength", len(body)))
		return "", "", "", fmt.Errorf("%w: tidb_server_connections returned no series for cluster_id=%s", errMetricLabelsNoSeries, clusterID)
	}
	keyspaceID, ok := result.Data[0][labelKeyspaceID]
	if !ok || keyspaceID == "" {
		log.Error(ctx, "tidb_server_connections series has no keyspace_id label",
			zap.String("clusterID", clusterID))
		return "", "", "", fmt.Errorf("tidb_server_connections series has no keyspace_id label")
	}
	controlPlaneInfo, ok := result.Data[0][labelControlPlaneInfo]
	if !ok || controlPlaneInfo == "" {
		log.Error(ctx, "tidb_server_connections series has no control_plane_info label",
			zap.String("clusterID", clusterID))
		return "", "", "", fmt.Errorf("tidb_server_connections series has no control_plane_info label")
	}
	instances := make(map[string]struct{}, len(result.Data))
	for _, data := range result.Data {
		instance, ok := data[labelInstance]
		if !ok || instance == "" {
			continue
		}
		instances[instance] = struct{}{}
	}
	// join keys to string
	instancesKeys := slices.Collect(maps.Keys(instances))
	return keyspaceID, strings.Join(instancesKeys, "|"), controlPlaneInfo, nil
}

// buildQueryWithLabels returns the PromQL query with deployTypeV2-specific labels injected.
// start and end are used for Starter/Essential keyspace_id lookup (query_range). If both are zero, last 1 day is used.
func (s *MetricsQueryService) buildQueryWithLabels(ctx context.Context, query, clusterID string, clusterURLs *clinic_internal_api.ClusterDataURLs, category dto.MetricCategory, start, end time.Time) (string, error) {
	// BYOC data-plane URLs already resolve to the customer-owned Kubernetes
	// observability cluster. Infrastructure metrics such as kube_pod_info and
	// node_* do not carry tidb_cluster_id, so adding that selector makes the
	// same PromQL used by the Nodes Info dashboard return an empty result.
	// Keep this exception BYOC-only; label injection for every other tier and
	// for BYOC cluster metrics remains unchanged.
	if category == dto.MetricCategoryDataPlane && isExactBYOCMetricsTarget(clusterURLs) {
		return query, nil
	}

	deployTypeV2 := strings.ToLower(strings.TrimSpace(clusterURLs.DeployTypeV2))
	if isStarterOrEssential(deployTypeV2) {
		if clusterURLs.ClusterMetricsURL == "" {
			return "", fmt.Errorf("cluster metrics URL required for Starter/Essential keyspace_id lookup")
		}

		keyspaceID, instances, controlPlaneInfo, err := s.fetchMetricLabels(ctx, clusterURLs.ClusterMetricsURL, clusterID, start, end)
		if err != nil {
			return "", fmt.Errorf("fetch keyspace_id for Starter/Essential: %w", err)
		}
		log.Info(ctx, "injecting keyspace_id for Starter/Essential",
			zap.String("clusterID", clusterID),
			zap.String("keyspace_id", keyspaceID))
		// Build metric -> (label name, label value) map for Starter/Essential; default is keyspace_id for all metrics.
		metricLabels := map[string]MetricLabel{
			"resource_manager_resource_unit_read_request_unit_sum":  {Name: labelName, MatchType: labels.MatchEqual, Value: keyspaceID},
			"resource_manager_resource_unit_write_request_unit_sum": {Name: labelName, MatchType: labels.MatchEqual, Value: keyspaceID},
			"resource_manager_resource_unit_sql_layer_request_unit": {Name: labelName, MatchType: labels.MatchEqual, Value: keyspaceID},
			"gateway_connection_flow_bytes_total":                   {Name: labelClusterID, MatchType: labels.MatchEqual, Value: clusterID},
			"process_start_time_seconds":                            {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"process_cpu_seconds_total":                             {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"process_resident_memory_bytes":                         {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"go_memory_classes_heap_objects_bytes":                  {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"go_memory_classes_heap_released_bytes":                 {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"go_memory_classes_heap_unused_bytes":                   {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"go_memory_classes_heap_free_bytes":                     {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"go_gc_heap_goal_bytes":                                 {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"go_gc_cycles_total_gc_cycles_total":                    {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"process_open_fds":                                      {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"go_sched_goroutines_goroutines":                        {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"pd_client_request_forwarded_status":                    {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"http_requests_total":                                   {Name: labelInstance, MatchType: labels.MatchRegexp, Value: instances},
			"kube_pod_status_phase":                                 {Name: labelControlPlaneInfo, MatchType: labels.MatchEqual, Value: controlPlaneInfo},
			"pd_scheduler_store_status":                             {Name: labelControlPlaneInfo, MatchType: labels.MatchEqual, Value: controlPlaneInfo},
			"kv_engine_*":                                           {Name: labelControlPlaneInfo, MatchType: labels.MatchEqual, Value: controlPlaneInfo},
			"tikv_*":                                                {Name: labelControlPlaneInfo, MatchType: labels.MatchEqual, Value: controlPlaneInfo},
		}
		// Per-metric overrides can be added here, e.g. metricLabels["some_metric"] = MetricLabel{LabelName: "other_label", LabelValue: "val"}
		defaultLabel := MetricLabel{Name: labelKeyspaceID, MatchType: labels.MatchEqual, Value: keyspaceID}
		return injectLabelIntoPromQL(query, defaultLabel, metricLabels), nil
	}
	if isPremiumOrByoc(deployTypeV2) {
		var tidbClusterIDValue string
		if category == dto.MetricCategoryCluster {
			tidbClusterIDValue = clusterID
		} else {
			tidbClusterIDValue = clusterURLs.ParentID
			if tidbClusterIDValue == "" {
				return "", fmt.Errorf("ParentID is required for Premium/Byoc resource pool")
			}
		}
		log.Info(ctx, "injecting tidb_cluster_id for Premium/Byoc",
			zap.String("clusterID", clusterID),
			zap.String("category", string(category)),
			zap.String("tidb_cluster_id", tidbClusterIDValue))
		defaultLabel := MetricLabel{Name: labelTidbClusterID, MatchType: labels.MatchEqual, Value: tidbClusterIDValue}
		return injectLabelIntoPromQL(query, defaultLabel, nil), nil
	}
	if isPremiumOrByocResourcePool(deployTypeV2) {
		defaultLabel := MetricLabel{Name: labelTidbClusterID, MatchType: labels.MatchEqual, Value: clusterID}
		return injectLabelIntoPromQL(query, defaultLabel, nil), nil
	}
	return query, nil
}

// getPrometheusURLAndClusterInfo returns the Prometheus base URL for the query and cluster metadata for label injection.
func (s *MetricsQueryService) getPrometheusURLAndClusterInfo(ctx context.Context, clusterID string, queries ...string) (baseURL string, clusterURLs *clinic_internal_api.ClusterDataURLs, category dto.MetricCategory, err error) {
	clusterURLs, err = s.clinicClient.GetClusterDataURLs(ctx, clusterID)
	if err != nil {
		return "", nil, "", fmt.Errorf("failed to fetch cluster data URLs: %w", err)
	}
	return s.getPrometheusURLForResolvedTarget(ctx, clusterID, clusterURLs, queries...)
}

func (s *MetricsQueryService) getPrometheusURLForResolvedTarget(
	ctx context.Context,
	clusterID string,
	clusterURLs *clinic_internal_api.ClusterDataURLs,
	queries ...string,
) (baseURL string, target *clinic_internal_api.ClusterDataURLs, category dto.MetricCategory, err error) {
	if clusterURLs == nil || clusterURLs.ClusterID != clusterID {
		return "", clusterURLs, "", fmt.Errorf("resolved cluster target identity mismatch")
	}
	log.Info(ctx, "got cluster data URLs",
		zap.String("clusterID", clusterID),
		zap.String("deployTypeV2", clusterURLs.DeployTypeV2))

	category, err = s.classifyMetricCategory(queries...)
	if err != nil {
		return "", clusterURLs, "", err
	}
	var prometheusURL string
	switch category {
	case dto.MetricCategoryDataPlane:
		prometheusURL = clusterURLs.DataPlaneMetricsURL
	default:
		prometheusURL = clusterURLs.ClusterMetricsURL
	}
	if prometheusURL == "" {
		return "", clusterURLs, "", fmt.Errorf("no prometheus URL available for category: %s", category)
	}
	log.Info(ctx, "selected prometheus endpoint",
		zap.String("clusterID", clusterID),
		zap.String("category", string(category)))
	return normalizePrometheusURL(prometheusURL), clusterURLs, category, nil
}

func isExactBYOCMetricsTarget(target *clinic_internal_api.ClusterDataURLs) bool {
	if target == nil {
		return false
	}
	return (target.DeployType == "byoc" && target.DeployTypeV2 == deployTypeV2Byoc) ||
		(target.DeployType == "byoc-host" && target.DeployTypeV2 == deployTypeV2ByocResourcePool)
}

func validBYOCMetricsRoutingValue(value string) bool {
	if value == "" || strings.TrimSpace(value) != value || len(value) > 255 {
		return false
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return false
		}
	}
	return true
}

func validBYOCMetricsPlacementValue(value string) bool {
	if !validBYOCMetricsRoutingValue(value) {
		return false
	}
	for _, r := range value {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') ||
			r == '-' || r == '_' || r == '.' {
			continue
		}
		return false
	}
	return true
}

func metricsQueryErrorType(err error) string {
	if errors.Is(err, errCrossMetricSource) {
		return "unsupported_query"
	}
	return "internal"
}

func emptyPrometheusQueryResponse(resultType string, clusterURLs *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	return withMetricsRouteInfo(&dto.PrometheusResponse{
		Status: "success",
		Data: map[string]any{
			"resultType": resultType,
			"result":     []any{},
		},
	}, clusterURLs)
}

func emptyPrometheusSeriesResponse(clusterURLs *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	return withMetricsRouteInfo(&dto.PrometheusResponse{
		Status: "success",
		Data:   []any{},
	}, clusterURLs)
}

// InstantQuery executes an instant query (GET/POST /api/v1/query)
func (s *MetricsQueryService) InstantQuery(ctx context.Context, req *dto.InstantQueryRequest) *dto.PrometheusResponse {
	return s.instantQuery(ctx, req, nil)
}

// InstantQueryWithResolvedTarget executes the same instant-query core using a
// server-resolved, identity-checked cluster target.
func (s *MetricsQueryService) InstantQueryWithResolvedTarget(ctx context.Context, req *dto.InstantQueryRequest, target *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	return s.instantQuery(ctx, req, target)
}

func (s *MetricsQueryService) instantQuery(ctx context.Context, req *dto.InstantQueryRequest, resolvedTarget *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	if req.Query == "" {
		return &dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "query parameter is required",
		}
	}

	var baseURL string
	var clusterURLs *clinic_internal_api.ClusterDataURLs
	var category dto.MetricCategory
	var err error
	if resolvedTarget == nil {
		baseURL, clusterURLs, category, err = s.getPrometheusURLAndClusterInfo(ctx, req.ClusterID, req.Query)
	} else {
		baseURL, clusterURLs, category, err = s.getPrometheusURLForResolvedTarget(ctx, req.ClusterID, resolvedTarget, req.Query)
	}
	if err != nil {
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:    "error",
			ErrorType: metricsQueryErrorType(err),
			Error:     err.Error(),
		}, clusterURLs)
	}

	queryToUse, err := s.buildQueryWithLabels(ctx, req.Query, req.ClusterID, clusterURLs, category, time.Time{}, time.Time{})
	if err != nil {
		if errors.Is(err, errMetricLabelsNoSeries) {
			return emptyPrometheusQueryResponse("vector", clusterURLs)
		}
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "internal",
			Error:     err.Error(),
		}, clusterURLs)
	}

	// Build query URL - the base URL already ends with /prometheus/, just append "query"
	queryURL := strings.TrimRight(baseURL, "/") + "/query"

	// Build query parameters
	params := make(map[string]string)
	params["query"] = queryToUse

	// Use provided time or default to now
	queryTime := req.Time
	if !queryTime.IsZero() {
		params["time"] = fmt.Sprintf("%d", queryTime.Unix())
	}

	if req.Timeout != "" {
		params["timeout"] = req.Timeout
	}

	return s.executeQuery(ctx, queryURL, params, clusterURLs)
}

// RangeQuery executes a range query (GET/POST /api/v1/query_range)
func (s *MetricsQueryService) RangeQuery(ctx context.Context, req *dto.RangeQueryRequest) *dto.PrometheusResponse {
	return s.rangeQuery(ctx, req, nil)
}

// RangeQueryWithResolvedTarget executes the same range-query core using a
// server-resolved, identity-checked cluster target.
func (s *MetricsQueryService) RangeQueryWithResolvedTarget(ctx context.Context, req *dto.RangeQueryRequest, target *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	return s.rangeQuery(ctx, req, target)
}

func (s *MetricsQueryService) rangeQuery(ctx context.Context, req *dto.RangeQueryRequest, resolvedTarget *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	if req.Query == "" {
		return &dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "query parameter is required",
		}
	}

	if req.Start.IsZero() || req.End.IsZero() {
		return &dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "start and end parameters are required",
		}
	}

	if req.End.Before(req.Start) {
		return &dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "end time must not be before start time",
		}
	}

	// Default step to 15s if not provided
	step := req.Step
	if step == 0 {
		step = 15 * time.Second
	}

	var baseURL string
	var clusterURLs *clinic_internal_api.ClusterDataURLs
	var category dto.MetricCategory
	var err error
	if resolvedTarget == nil {
		baseURL, clusterURLs, category, err = s.getPrometheusURLAndClusterInfo(ctx, req.ClusterID, req.Query)
	} else {
		baseURL, clusterURLs, category, err = s.getPrometheusURLForResolvedTarget(ctx, req.ClusterID, resolvedTarget, req.Query)
	}
	if err != nil {
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:    "error",
			ErrorType: metricsQueryErrorType(err),
			Error:     err.Error(),
		}, clusterURLs)
	}

	queryToUse, err := s.buildQueryWithLabels(ctx, req.Query, req.ClusterID, clusterURLs, category, req.Start, req.End)
	if err != nil {
		if errors.Is(err, errMetricLabelsNoSeries) {
			return emptyPrometheusQueryResponse("matrix", clusterURLs)
		}
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "internal",
			Error:     err.Error(),
		}, clusterURLs)
	}

	// Build query URL - the base URL already ends with /prometheus/, just append "query_range"
	queryURL := strings.TrimRight(baseURL, "/") + "/query_range"

	// Build query parameters
	params := make(map[string]string)
	params["query"] = queryToUse
	params["start"] = fmt.Sprintf("%d", req.Start.Unix())
	params["end"] = fmt.Sprintf("%d", req.End.Unix())
	params["step"] = fmt.Sprintf("%.0fs", step.Seconds())

	if req.Timeout != "" {
		params["timeout"] = req.Timeout
	}

	return s.executeQuery(ctx, queryURL, params, clusterURLs)
}

func withMetricsRouteInfo(resp *dto.PrometheusResponse, clusterURLs *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	if resp != nil {
		if clusterURLs != nil {
			resp.DeployTypeV2 = clusterURLs.DeployTypeV2
			resp.Provider = clusterURLs.Provider
			resp.Region = clusterURLs.Region
		}
	}
	return resp
}

// executeQueryWithValues makes an HTTP GET request with url.Values and returns the response.
func (s *MetricsQueryService) executeQueryWithValues(ctx context.Context, queryURL string, params url.Values, clusterURLs *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	u, err := url.Parse(queryURL)
	if err != nil {
		log.Error(ctx, "failed to parse query URL",
			zap.String("errorType", fmt.Sprintf("%T", err)))
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "internal",
			Error:     fmt.Sprintf("failed to parse query URL: %v", err),
		}, clusterURLs)
	}

	u.RawQuery = params.Encode()
	fullURL := u.String()

	log.Info(ctx, "executing metrics query",
		zap.String("upstreamHost", u.Host),
		zap.String("upstreamPath", u.Path))

	req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
	if err != nil {
		log.Error(ctx, "failed to create HTTP request",
			zap.String("errorType", fmt.Sprintf("%T", err)))
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "internal",
			Error:     fmt.Sprintf("failed to create request: %v", err),
		}, clusterURLs)
	}
	if isExactBYOCMetricsTarget(clusterURLs) {
		if err := setBYOCMetricsHeaders(req, clusterURLs); err != nil {
			return withMetricsRouteInfo(&dto.PrometheusResponse{
				Status:    "error",
				ErrorType: "internal",
				Error:     errInvalidBYOCMetricsTarget.Error(),
			}, clusterURLs)
		}
	}

	resp, err := s.httpClient.Do(req)
	if err != nil {
		upstreamStatus := http.StatusBadGateway
		var networkError net.Error
		if errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &networkError) && networkError.Timeout()) {
			upstreamStatus = http.StatusGatewayTimeout
		}
		log.Error(ctx, "HTTP request failed",
			zap.String("errorType", fmt.Sprintf("%T", err)),
			zap.Bool("timeout", upstreamStatus == http.StatusGatewayTimeout))
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:         "error",
			ErrorType:      "execution",
			Error:          fmt.Sprintf("request failed: %v", err),
			UpstreamStatus: upstreamStatus,
		}, clusterURLs)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Error(ctx, "failed to read response body",
			zap.Int("statusCode", resp.StatusCode),
			zap.Error(err))
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "internal",
			Error:     fmt.Sprintf("failed to read response: %v", err),
		}, clusterURLs)
	}

	log.Info(ctx, "received response from upstream",
		zap.Int("statusCode", resp.StatusCode),
		zap.Int("bodyLength", len(body)))

	if resp.StatusCode != http.StatusOK {
		upstreamError, diagnostics := newMetricsUpstreamErrorResponse(resp.StatusCode, body)
		log.Error(ctx, "upstream returned non-200 status",
			zap.Int("statusCode", resp.StatusCode),
			zap.Int("bodyLength", len(body)),
			zap.String("errorCode", diagnostics.Code),
			zap.String("upstreamErrorType", diagnostics.UpstreamErrorType),
			zap.Bool("sanitized", diagnostics.Sanitized))
		return withMetricsRouteInfo(upstreamError, clusterURLs)
	}

	var promResp dto.PrometheusResponse
	if err := json.Unmarshal(body, &promResp); err != nil {
		log.Error(ctx, "failed to parse response as Prometheus response",
			zap.Int("statusCode", resp.StatusCode),
			zap.Int("bodyLength", len(body)),
			zap.Error(err))
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:         "error",
			ErrorType:      "internal",
			Error:          "failed to parse metrics upstream response",
			UpstreamStatus: http.StatusBadGateway,
		}, clusterURLs)
	}

	if promResp.Status == "error" {
		log.Error(ctx, "upstream returned error in response",
			zap.String("errorType", promResp.ErrorType))
	} else {
		log.Info(ctx, "metrics query successful",
			zap.String("status", promResp.Status))
	}

	return withMetricsRouteInfo(&promResp, clusterURLs)
}

func setBYOCMetricsHeaders(req *http.Request, target *clinic_internal_api.ClusterDataURLs) error {
	if req == nil || target == nil || !isExactBYOCMetricsTarget(target) {
		return errInvalidBYOCMetricsTarget
	}
	if !validBYOCMetricsRoutingValue(target.ClinicTenantID) ||
		!validBYOCMetricsPlacementValue(target.Provider) ||
		!validBYOCMetricsPlacementValue(target.Region) {
		return errInvalidBYOCMetricsTarget
	}
	tenantID, err := strconv.ParseUint(target.ClinicTenantID, 10, 64)
	if err != nil || tenantID == 0 {
		return errInvalidBYOCMetricsTarget
	}
	regionID := target.Provider + "-" + target.Region
	if len(regionID) > 255 {
		return errInvalidBYOCMetricsTarget
	}
	req.Header.Set("X-Biz-Type", "byoc")
	req.Header.Set("X-Org-Id", target.ClinicTenantID)
	req.Header.Set("X-Region-Id", regionID)
	return nil
}

// executeQuery makes an HTTP GET request to the metrics endpoint and returns the response
func (s *MetricsQueryService) executeQuery(ctx context.Context, queryURL string, params map[string]string, clusterURLs *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	values := url.Values{}
	for k, v := range params {
		values.Set(k, v)
	}
	return s.executeQueryWithValues(ctx, queryURL, values, clusterURLs)
}

// Series executes a series query (GET/POST /api/v1/series)
func (s *MetricsQueryService) Series(ctx context.Context, req *dto.SeriesRequest) *dto.PrometheusResponse {
	return s.series(ctx, req, nil)
}

// SeriesWithResolvedTarget executes the same series-query core using a
// server-resolved, identity-checked cluster target.
func (s *MetricsQueryService) SeriesWithResolvedTarget(ctx context.Context, req *dto.SeriesRequest, target *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	return s.series(ctx, req, target)
}

func (s *MetricsQueryService) series(ctx context.Context, req *dto.SeriesRequest, resolvedTarget *clinic_internal_api.ClusterDataURLs) *dto.PrometheusResponse {
	if len(req.Match) == 0 {
		return &dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "match[] parameter is required",
		}
	}

	var baseURL string
	var clusterURLs *clinic_internal_api.ClusterDataURLs
	var category dto.MetricCategory
	var err error
	if resolvedTarget == nil {
		baseURL, clusterURLs, category, err = s.getPrometheusURLAndClusterInfo(ctx, req.ClusterID, req.Match...)
	} else {
		baseURL, clusterURLs, category, err = s.getPrometheusURLForResolvedTarget(ctx, req.ClusterID, resolvedTarget, req.Match...)
	}
	if err != nil {
		return withMetricsRouteInfo(&dto.PrometheusResponse{
			Status:    "error",
			ErrorType: metricsQueryErrorType(err),
			Error:     err.Error(),
		}, clusterURLs)
	}

	matchWithLabels := make([]string, 0, len(req.Match))
	for _, m := range req.Match {
		q, err := s.buildQueryWithLabels(ctx, m, req.ClusterID, clusterURLs, category, req.Start, req.End)
		if err != nil {
			if errors.Is(err, errMetricLabelsNoSeries) {
				return emptyPrometheusSeriesResponse(clusterURLs)
			}
			return withMetricsRouteInfo(&dto.PrometheusResponse{
				Status:    "error",
				ErrorType: "internal",
				Error:     err.Error(),
			}, clusterURLs)
		}
		matchWithLabels = append(matchWithLabels, q)
	}

	// Build query URL
	queryURL := strings.TrimRight(baseURL, "/") + "/series"

	// Build query parameters
	params := url.Values{}
	for _, m := range matchWithLabels {
		params.Add("match[]", m)
	}

	if !req.Start.IsZero() {
		params.Set("start", fmt.Sprintf("%d", req.Start.Unix()))
	}

	if !req.End.IsZero() {
		params.Set("end", fmt.Sprintf("%d", req.End.Unix()))
	}

	return s.executeQueryWithValues(ctx, queryURL, params, clusterURLs)
}

// normalizePrometheusURL ensures the URL is properly formatted
// The URL from cluster-dataurls ends with /prometheus/, we just use it directly
func normalizePrometheusURL(rawURL string) string {
	// Ensure http(s):// prefix
	if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
		rawURL = "https://" + rawURL
	}

	return rawURL
}
