// This file discovers Clinic placement and O11Y application and infrastructure data sources.
package evaluation

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

type clinicReplayCluster struct {
	ClusterProviderName string `json:"clusterProviderName"`
	Provider            string `json:"provider"`
	CloudProvider       string `json:"cloudProvider"`
	ClusterRegionName   string `json:"clusterRegionName"`
	Region              string `json:"region"`
	DeployType          string `json:"deployType"`
	ClusterDeployType   string `json:"clusterDeployType"`
	ParentID            string `json:"parentID"`
	ParentIDCamel       string `json:"parentId"`
	DiagnosticContext   struct {
		ParentID string `json:"parentId"`
	} `json:"diagnosticContext"`
}

type o11yDiscoveryResponse struct {
	Code int `json:"code"`
	Data struct {
		Clusters []o11yDiscoveryCluster `json:"clusters"`
	} `json:"data"`
}

type o11yDiscoveryCluster struct {
	ApplicationID   string `json:"applicationID"`
	BizType         string `json:"bizType"`
	ID              string `json:"id"`
	InternalReadURI string `json:"internalReadUri"`
	TenantID        string `json:"tenantID"`
	Vendor          string `json:"vendor"`
	Region          string `json:"region"`
}

func DiscoverReplayDatasetSpec(ctx context.Context, experiment *ReplayExperiment, profile *MetricsProfile, options ReplayDatasetResolveOptions) (*ReplayDatasetSpec, error) {
	if experiment == nil || profile == nil {
		return nil, fmt.Errorf("evaluation: replay experiment and metrics profile are required for O11Y discovery")
	}
	if options.Client == nil {
		options.Client = &http.Client{Timeout: defaultO11YHTTPTimeout}
	}
	if options.ClinicEndpoint == "" {
		options.ClinicEndpoint = defaultClinicEndpoint
	}
	if options.O11YGlobalEndpoint == "" {
		options.O11YGlobalEndpoint = defaultO11YGlobalEndpoint
	}
	if options.ClinicAPIKey == "" {
		return nil, fmt.Errorf("Clinic API key is required")
	}
	if options.O11YBearerToken == "" {
		return nil, fmt.Errorf("O11Y bearer token is required")
	}

	logicalClusterID := experiment.Spec.Target.ClusterID
	metadata, err := discoverClinicReplayCluster(ctx, options.Client, options.ClinicEndpoint, options.ClinicAPIKey, logicalClusterID)
	if err != nil {
		return nil, err
	}
	vendor := firstNonEmpty(metadata.ClusterProviderName, metadata.Provider, metadata.CloudProvider)
	region := firstNonEmpty(metadata.ClusterRegionName, metadata.Region)
	deployType := firstNonEmpty(metadata.DeployType, metadata.ClusterDeployType)
	physicalClusterID := firstNonEmpty(metadata.ParentID, metadata.ParentIDCamel, metadata.DiagnosticContext.ParentID)
	applicationID, err := replayApplicationID(logicalClusterID, physicalClusterID, deployType)
	if err != nil {
		return nil, err
	}
	if vendor == "" || region == "" {
		return nil, fmt.Errorf("Clinic metadata for cluster %q is missing provider or region", logicalClusterID)
	}

	application, err := discoverO11YApplication(ctx, options.Client, options.O11YGlobalEndpoint, options.O11YBearerToken, vendor, region, applicationID)
	if err != nil {
		return nil, err
	}
	hpaName := ""
	if experiment.Spec.HPADesiredReplicasMetric != "" {
		hpaName, err = discoverTiDBHPAName(ctx, options.Client, options.O11YBearerToken, application, logicalClusterID, experiment.Spec.Window.End)
		if err != nil {
			return nil, err
		}
	}
	namespace := "tidb" + applicationID
	infrastructure, err := discoverO11YInfrastructure(ctx, options.Client, options.O11YGlobalEndpoint, options.O11YBearerToken, application, namespace, experiment.Spec.Window.End)
	if err != nil {
		return nil, err
	}

	target := O11YTargetSpec{ClusterID: logicalClusterID, Vendor: vendor, Region: region}
	window := O11YQueryWindow{
		Start:       experiment.Spec.Window.Start,
		End:         experiment.Spec.Window.End,
		StepSeconds: experiment.Spec.DataStepSeconds,
	}
	endpoints := make(map[O11YMetricSource]string, len(profile.Sources))
	for i := range profile.Sources {
		switch profile.Sources[i].Type {
		case O11YMetricSourceApplication:
			endpoints[O11YMetricSourceApplication] = strings.TrimRight(application.InternalReadURI, "/")
		case O11YMetricSourceInfrastructure:
			endpoints[O11YMetricSourceInfrastructure] = strings.TrimRight(infrastructure.InternalReadURI, "/")
		}
	}

	referenceSources := make([]O11YSourceSpec, 0, 1)
	if experiment.Spec.HPADesiredReplicasMetric != "" {
		referenceSources = append(referenceSources, O11YSourceSpec{
			Type:    O11YMetricSourceInfrastructure,
			BaseURL: strings.TrimRight(infrastructure.InternalReadURI, "/"),
			Headers: o11yInfrastructureHeaders(application),
			Queries: []O11YQuerySpec{{
				Name: experiment.Spec.HPADesiredReplicasMetric,
				PromQL: fmt.Sprintf("sum(kube_horizontalpodautoscaler_status_desired_replicas{namespace=%q,horizontalpodautoscaler=%q})",
					namespace, hpaName),
				Type:          O11YQueryTypeRange,
				Required:      true,
				KeepLabels:    []string{},
				MaxGapSeconds: defaultReferenceMaxGap,
			}},
		})
	}

	return &ReplayDatasetSpec{
		Version:          ReplayVersionV1Alpha1,
		ID:               newReplayDatasetID(experiment.Spec.Component, logicalClusterID, experiment.Spec.MetricsProfile, window),
		Component:        experiment.Spec.Component,
		Profile:          experiment.Spec.MetricsProfile,
		Target:           target,
		Window:           window,
		Endpoints:        endpoints,
		ReferenceSources: referenceSources,
	}, nil
}

const (
	replayTiDBInstanceSeparator = "-tidb-"
	replayTiDBHPANamePrefix     = "tidbgroup-"
)

func discoverTiDBHPAName(ctx context.Context, client *http.Client, token string, application *o11yDiscoveryCluster, clusterID string, at time.Time) (string, error) {
	source := &O11YSourceSpec{
		Type:    O11YMetricSourceApplication,
		BaseURL: application.InternalReadURI,
	}
	query := &O11YQuerySpec{
		Name:   "tidb_hpa_name_probe",
		PromQL: fmt.Sprintf("up{tidb_cluster_id=%q,component=%q} == 1", clusterID, "tidb"),
		Type:   O11YQueryTypeInstant,
	}
	requestURL, err := buildO11YQueryURL(source, query, &O11YQueryWindow{Start: at, End: at}, at)
	if err != nil {
		return "", err
	}
	body, err := fetchO11YQueryWithHeaders(ctx, client, token, requestURL, nil)
	if err != nil {
		return "", fmt.Errorf("discover TiDB HPA name: %w", err)
	}
	response, err := decodePrometheusResponse(body)
	if err != nil {
		return "", fmt.Errorf("discover TiDB HPA name: %w", err)
	}
	if response.IsPartial || response.Data.ResultType != "vector" {
		return "", fmt.Errorf("discover TiDB HPA name returned partial or non-vector data")
	}

	names := make(map[string]struct{}, 1)
	for i := range response.Data.Result {
		instance := response.Data.Result[i].Metric[metricInstanceLabel]
		separator := strings.LastIndex(instance, replayTiDBInstanceSeparator)
		if separator < 1 {
			return "", fmt.Errorf("discover TiDB HPA name: instance %q does not match TiDB naming", instance)
		}
		names[replayTiDBHPANamePrefix+instance[:separator]] = struct{}{}
	}
	if len(names) != 1 {
		return "", fmt.Errorf("discover TiDB HPA name resolved %d names for cluster %q", len(names), clusterID)
	}
	for name := range names {
		return name, nil
	}
	return "", fmt.Errorf("discover TiDB HPA name for cluster %q", clusterID)
}

func discoverClinicReplayCluster(ctx context.Context, client *http.Client, endpoint, apiKey, clusterID string) (*clinicReplayCluster, error) {
	requestURL := strings.TrimRight(endpoint, "/") + "/data-proxy/api/v1/clusters/" + url.PathEscape(clusterID)
	body, err := fetchDiscoveryJSON(ctx, client, apiKey, requestURL, nil)
	if err != nil {
		return nil, fmt.Errorf("discover Clinic cluster %q: %w", clusterID, err)
	}

	var metadata clinicReplayCluster
	if err := json.Unmarshal(body, &metadata); err != nil {
		return nil, fmt.Errorf("decode Clinic cluster %q: %w", clusterID, err)
	}
	return &metadata, nil
}

func discoverO11YApplication(ctx context.Context, client *http.Client, endpoint, token, vendor, region, applicationID string) (*o11yDiscoveryCluster, error) {
	clusters, err := queryO11YDiscovery(ctx, client, endpoint, token, url.Values{
		"vendor":        []string{vendor},
		"region":        []string{region},
		"applicationID": []string{applicationID},
	})
	if err != nil {
		return nil, fmt.Errorf("discover O11Y application %q: %w", applicationID, err)
	}
	matches := make([]o11yDiscoveryCluster, 0, 1)
	for i := range clusters {
		if clusters[i].ApplicationID == applicationID {
			matches = append(matches, clusters[i])
		}
	}
	if len(matches) != 1 || matches[0].InternalReadURI == "" || matches[0].TenantID == "" || matches[0].BizType == "" {
		return nil, fmt.Errorf("O11Y application %q resolved to %d complete entries", applicationID, len(matches))
	}
	return &matches[0], nil
}

func discoverO11YInfrastructure(ctx context.Context, client *http.Client, endpoint, token string, application *o11yDiscoveryCluster, namespace string, at time.Time) (*o11yDiscoveryCluster, error) {
	clusters, err := queryO11YDiscovery(ctx, client, endpoint, token, url.Values{
		"vendor":   []string{application.Vendor},
		"region":   []string{application.Region},
		"tenantID": []string{application.TenantID},
	})
	if err != nil {
		return nil, fmt.Errorf("discover O11Y infrastructure: %w", err)
	}
	candidates := make([]o11yDiscoveryCluster, 0, len(clusters))
	for i := range clusters {
		if strings.HasPrefix(clusters[i].ApplicationID, "dp-") && clusters[i].InternalReadURI != "" {
			candidates = append(candidates, clusters[i])
		}
	}
	if len(candidates) == 1 {
		return &candidates[0], nil
	}
	if len(candidates) == 0 {
		return nil, fmt.Errorf("no O11Y infrastructure entry found for tenant %q", application.TenantID)
	}

	matched := make([]o11yDiscoveryCluster, 0, 1)
	for i := range candidates {
		candidate := &candidates[i]
		source := &O11YSourceSpec{
			Type:    O11YMetricSourceInfrastructure,
			BaseURL: candidate.InternalReadURI,
			Headers: o11yInfrastructureHeaders(application),
		}
		query := &O11YQuerySpec{
			Name:   "hpa_current_replicas_probe",
			PromQL: fmt.Sprintf("kube_horizontalpodautoscaler_status_current_replicas{namespace=%q}", namespace),
			Type:   O11YQueryTypeInstant,
		}
		requestURL, err := buildO11YQueryURL(source, query, &O11YQueryWindow{Start: at, End: at}, at)
		if err != nil {
			return nil, err
		}
		body, err := fetchO11YQueryWithHeaders(ctx, client, token, requestURL, source.Headers)
		if err != nil {
			continue
		}
		response, err := decodePrometheusResponse(body)
		if err == nil && len(response.Data.Result) > 0 {
			matched = append(matched, *candidate)
		}
	}
	if len(matched) != 1 {
		return nil, fmt.Errorf("O11Y infrastructure probe matched %d of %d entries", len(matched), len(candidates))
	}
	return &matched[0], nil
}

func queryO11YDiscovery(ctx context.Context, client *http.Client, endpoint, token string, values url.Values) ([]o11yDiscoveryCluster, error) {
	requestURL, err := url.Parse(strings.TrimRight(endpoint, "/") + "/api/v1/clusters/")
	if err != nil {
		return nil, fmt.Errorf("build O11Y discovery URL: %w", err)
	}
	requestURL.RawQuery = values.Encode()
	body, err := fetchDiscoveryJSON(ctx, client, token, requestURL.String(), nil)
	if err != nil {
		return nil, err
	}

	var response o11yDiscoveryResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, fmt.Errorf("decode O11Y discovery response: %w", err)
	}
	if response.Code != 0 {
		return nil, fmt.Errorf("O11Y discovery code %d", response.Code)
	}
	return response.Data.Clusters, nil
}

func fetchDiscoveryJSON(ctx context.Context, client *http.Client, token, requestURL string, headers map[string]string) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+token)
	for name, value := range headers {
		req.Header.Set(name, value)
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxO11YResponseBytes+1))
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}
	if len(body) > maxO11YResponseBytes {
		return nil, fmt.Errorf("response exceeds %d bytes", maxO11YResponseBytes)
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		return nil, fmt.Errorf("HTTP status %s", resp.Status)
	}
	return body, nil
}

func replayApplicationID(clusterID, parentID, deployType string) (string, error) {
	normalized := strings.ToLower(strings.NewReplacer(" ", "_", "-", "_").Replace(deployType))
	switch {
	case strings.Contains(normalized, "premium"):
		if parentID == "" {
			return "", fmt.Errorf("Clinic metadata for Premium cluster %q is missing parent ID", clusterID)
		}
		return parentID, nil
	case normalized == "dedicated", strings.Contains(normalized, "resource_pool"):
		return clusterID, nil
	default:
		return "", fmt.Errorf("cluster %q deploy type %q is unsupported for O11Y replay", clusterID, deployType)
	}
}

func o11yInfrastructureHeaders(application *o11yDiscoveryCluster) map[string]string {
	return map[string]string{
		"x-org-id":    application.TenantID,
		"x-biz-type":  application.BizType,
		"x-region-id": application.Vendor + "-" + application.Region,
	}
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if value != "" {
			return value
		}
	}
	return ""
}
