package metrics

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

const (
	unknownKeyspaceName      = "unknown"
	unknownClusterID         = "-1"
	defaultEssencialMetaAddr = "http://meta-store.tidb-admin.svc.cluster.local:9088/api/v1/meta"
)

// MetricLabels contains the additional labels exported on all metrics.
type MetricLabels struct {
	KeyspaceName string
	ClusterID    string
}

// ClusterIDProvider resolves a cluster ID for a keyspace.
type ClusterIDProvider interface {
	ClusterID(ctx context.Context, keyspaceName string) (string, error)
}

// EssencialClusterIDProvider resolves keyspace metadata from the meta-store service.
type EssencialClusterIDProvider struct {
	baseURL string
	client  *http.Client
}

type cachedMetricLabels struct {
	once   sync.Once
	labels MetricLabels
}

type clusterIDProviderHolder struct {
	provider ClusterIDProvider
}

type staticClusterIDProvider struct {
	clusterID string
	err       error
}

type hostDerivedMetricLabelsHolder struct {
	enabled bool
	labels  MetricLabels
}

type metricUsernameContextKey struct{}

type essencialMetaResponse struct {
	KeyspaceName string `json:"KeyspaceName"`
	ClusterID    string `json:"ClusterId"`
}

var (
	clusterIDProvider atomic.Value // ClusterIDProvider
	hostDerivedLabels atomic.Value // hostDerivedMetricLabelsHolder
	labelCache        sync.Map     // map[string]*cachedMetricLabels
)

func init() {
	clusterIDProvider.Store(clusterIDProviderHolder{provider: newDefaultEssencialClusterIDProvider()})
	hostDerivedLabels.Store(hostDerivedMetricLabelsHolder{})
}

func newDefaultEssencialClusterIDProvider() *EssencialClusterIDProvider {
	transport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   500 * time.Millisecond,
			KeepAlive: 30 * time.Second,
		}).DialContext,
		ResponseHeaderTimeout: 500 * time.Millisecond,
		IdleConnTimeout:       30 * time.Second,
	}

	return &EssencialClusterIDProvider{
		baseURL: defaultEssencialMetaAddr,
		client: &http.Client{
			Timeout:   time.Second,
			Transport: transport,
		},
	}
}

// ClusterID resolves the cluster ID for a keyspace through the meta-store API.
func (p *EssencialClusterIDProvider) ClusterID(ctx context.Context, keyspaceName string) (string, error) {
	if p == nil {
		return "", fmt.Errorf("cluster id provider is nil")
	}
	keyspaceName = normalizeKeyspaceName(keyspaceName)
	if keyspaceName == unknownKeyspaceName {
		return "", fmt.Errorf("keyspace name is required")
	}

	baseURL := strings.TrimSpace(p.baseURL)
	if baseURL == "" {
		baseURL = defaultEssencialMetaAddr
	}

	endpoint, err := url.Parse(baseURL)
	if err != nil {
		return "", fmt.Errorf("parse essencial meta url: %w", err)
	}
	query := endpoint.Query()
	query.Set("keyspace_name", keyspaceName)
	endpoint.RawQuery = query.Encode()

	req, err := http.NewRequestWithContext(ctxWithDefault(ctx), http.MethodGet, endpoint.String(), nil)
	if err != nil {
		return "", fmt.Errorf("build essencial meta request: %w", err)
	}

	client := p.client
	if client == nil {
		client = newDefaultEssencialClusterIDProvider().client
	}

	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("unexpected status: %s", resp.Status)
	}

	var payload []essencialMetaResponse
	if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&payload); err != nil {
		return "", fmt.Errorf("decode essencial meta response: %w", err)
	}
	for _, item := range payload {
		if strings.TrimSpace(item.KeyspaceName) != keyspaceName {
			continue
		}
		clusterID := strings.TrimSpace(item.ClusterID)
		if clusterID == "" {
			return "", fmt.Errorf("cluster id missing for keyspace %q", keyspaceName)
		}
		return clusterID, nil
	}
	return "", fmt.Errorf("cluster id not found for keyspace %q", keyspaceName)
}

// WithUsername stores the raw username on the context and updates the request tracker when present.
func WithUsername(ctx context.Context, username string) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	username = strings.TrimSpace(username)
	if username == "" {
		return ctx
	}
	if tracker := FromContext(ctx); tracker != nil {
		tracker.SetMetricUsername(username)
	}
	return context.WithValue(ctx, metricUsernameContextKey{}, username)
}

func usernameFromContext(ctx context.Context) string {
	if ctx == nil {
		return ""
	}
	if value := ctx.Value(metricUsernameContextKey{}); value != nil {
		if username, ok := value.(string); ok {
			return strings.TrimSpace(username)
		}
	}
	if tracker := FromContext(ctx); tracker != nil {
		return tracker.MetricUsername()
	}
	return ""
}

func labelsFromContext(ctx context.Context) MetricLabels {
	if tracker := FromContext(ctx); tracker != nil {
		return tracker.MetricLabels(ctx)
	}
	return ResolveMetricLabels(ctx, usernameFromContext(ctx))
}

func labelsFromRequestMetrics(ctx context.Context, tracker *RequestMetrics) MetricLabels {
	if tracker != nil {
		return tracker.MetricLabels(ctx)
	}
	return labelsFromContext(ctx)
}

// ResolveMetricLabels resolves metric labels for a username and caches the result for the process lifetime.
func ResolveMetricLabels(ctx context.Context, username string) MetricLabels {
	if labels, ok := activeHostDerivedMetricLabels(); ok {
		return labels.withDefaults()
	}

	keyspaceName := normalizeKeyspaceName(username)
	if keyspaceName == unknownKeyspaceName {
		return defaultMetricLabels()
	}

	entryAny, _ := labelCache.LoadOrStore(keyspaceName, &cachedMetricLabels{})
	entry := entryAny.(*cachedMetricLabels)
	entry.once.Do(func() {
		labels := MetricLabels{
			KeyspaceName: keyspaceName,
			ClusterID:    unknownClusterID,
		}
		if provider := activeClusterIDProvider(); provider != nil {
			if clusterID, err := provider.ClusterID(ctxWithDefault(ctx), keyspaceName); err == nil {
				clusterID = strings.TrimSpace(clusterID)
				if clusterID != "" {
					labels.ClusterID = clusterID
				}
			}
		}
		entry.labels = labels
	})
	return entry.labels.withDefaults()
}

// SetClusterIDProviderForTest overrides the cluster ID provider and clears cached mappings.
func SetClusterIDProviderForTest(provider ClusterIDProvider) {
	if provider == nil {
		provider = staticClusterIDProvider{err: fmt.Errorf("cluster id provider disabled")}
	}
	clusterIDProvider.Store(clusterIDProviderHolder{provider: provider})
	SetMetricLabelsFromMySQLHost("")
	clearMetricLabelCache()
}

// SetMetricLabelsFromMySQLHost configures a host-derived label strategy when mysql.host matches
// db-{cluster-id}-tiproxy.<domain>. In that case both KeyspaceName and ClusterId use the extracted cluster id.
func SetMetricLabelsFromMySQLHost(host string) {
	holder := hostDerivedMetricLabelsHolder{}
	if labels, ok := deriveMetricLabelsFromMySQLHost(host); ok {
		holder.enabled = true
		holder.labels = labels.withDefaults()
	}
	hostDerivedLabels.Store(holder)
	clearMetricLabelCache()
}

func (p staticClusterIDProvider) ClusterID(ctx context.Context, keyspaceName string) (string, error) {
	if p.err != nil {
		return "", p.err
	}
	return strings.TrimSpace(p.clusterID), nil
}

func activeClusterIDProvider() ClusterIDProvider {
	if value := clusterIDProvider.Load(); value != nil {
		if holder, ok := value.(clusterIDProviderHolder); ok {
			return holder.provider
		}
	}
	return nil
}

func activeHostDerivedMetricLabels() (MetricLabels, bool) {
	if value := hostDerivedLabels.Load(); value != nil {
		if holder, ok := value.(hostDerivedMetricLabelsHolder); ok && holder.enabled {
			return holder.labels.withDefaults(), true
		}
	}
	return MetricLabels{}, false
}

func clearMetricLabelCache() {
	labelCache.Range(func(key, _ any) bool {
		labelCache.Delete(key)
		return true
	})
}

func normalizeKeyspaceName(username string) string {
	username = strings.TrimSpace(username)
	if username == "" {
		return unknownKeyspaceName
	}
	if keyspaceName, suffix, found := strings.Cut(username, "."); found && keyspaceName != "" && suffix != "" {
		return keyspaceName
	}
	return username
}

func deriveMetricLabelsFromMySQLHost(host string) (MetricLabels, bool) {
	host = strings.TrimSpace(host)
	if host == "" {
		return MetricLabels{}, false
	}

	if parsedHost, _, err := net.SplitHostPort(host); err == nil {
		host = parsedHost
	}
	host = strings.Trim(host, "[]")
	host = strings.TrimSpace(host)
	if host == "" {
		return MetricLabels{}, false
	}

	const (
		prefix = "db-"
		suffix = "-tiproxy"
	)
	if !strings.HasPrefix(host, prefix) {
		return MetricLabels{}, false
	}

	rest := host[len(prefix):]
	idx := strings.Index(rest, suffix)
	if idx <= 0 {
		return MetricLabels{}, false
	}
	clusterID := strings.TrimSpace(rest[:idx])
	if clusterID == "" || strings.Contains(clusterID, ".") {
		return MetricLabels{}, false
	}

	remainder := rest[idx+len(suffix):]
	if len(remainder) <= 1 || !strings.HasPrefix(remainder, ".") {
		return MetricLabels{}, false
	}

	return MetricLabels{
		KeyspaceName: clusterID,
		ClusterID:    clusterID,
	}, true
}

func defaultMetricLabels() MetricLabels {
	return MetricLabels{
		KeyspaceName: unknownKeyspaceName,
		ClusterID:    unknownClusterID,
	}
}

func (l MetricLabels) withDefaults() MetricLabels {
	if strings.TrimSpace(l.KeyspaceName) == "" {
		l.KeyspaceName = unknownKeyspaceName
	}
	if strings.TrimSpace(l.ClusterID) == "" {
		l.ClusterID = unknownClusterID
	}
	return l
}

func ctxWithDefault(ctx context.Context) context.Context {
	if ctx == nil {
		return context.Background()
	}
	return ctx
}
