// Package observability exposes Prometheus metrics and structured logging for Data Proxy.
package observability

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"go.uber.org/zap"
)

const (
	// CallerHeaderCandidates are checked in order; callers should set one for dashboard "who".
	headerClinicCaller = "ClinicUserKey"
)

var (
	dataProxyDurationBuckets = []float64{
		0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300,
	}

	// Full-detail counter for "who / deploy type / error code" panels.
	dataProxyRequestsTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: "clinic",
			Subsystem: "data-proxy",
			Name:      "http_requests_total",
			Help:      "Data Proxy HTTP requests by caller, deploy type, query type, provider, region, engine, and outcome.",
		},
		[]string{
			"endpoint", "caller", "query_type", "deploy_type_v2", "provider", "region",
			"engine", "status_class", "error_code", "http_status",
		},
	)

	// Histogram for latency percentiles (no caller).
	dataProxyRequestDuration = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: "clinic",
			Subsystem: "data-proxy",
			Name:      "http_request_duration_seconds",
			Help:      "Data Proxy request wall time (handler), by endpoint and routing attributes.",
			Buckets:   dataProxyDurationBuckets,
		},
		[]string{"endpoint", "query_type", "deploy_type_v2", "provider", "region", "engine", "status_class"},
	)

	dataProxySchemaRefreshTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: "clinic",
			Subsystem: "data-proxy",
			Name:      "schema_refresh_total",
			Help:      "Data Proxy external table schema refresh attempts by provider and outcome.",
		},
		[]string{"provider", "outcome"},
	)
)

// DataProxyRecord labels one Data Proxy HTTP request for metrics and logs.
type DataProxyRecord struct {
	Endpoint     string // e.g. query, batch_query, schema, metrics_instant, loki_query_range
	Caller       string
	QueryType    string
	DeployTypeV2 string // from cluster-dataurls when available
	Provider     string
	Region       string
	Engine       string
	ErrCode      string // stable code for failures; empty on success
	HTTPStatus   int    // set after WriteHeader / JSON; 0 if unknown
}

type SessionUser struct {
	Username string `json:"username"`
	UserID   uint   `json:"user_id"`
	Email    string `json:"email"`
	Internal bool   `json:"internal"`
}

// CallerFromGin returns a stable caller id from common headers, or "anonymous".
func CallerFromGin(c *gin.Context) string {
	if c == nil {
		return "anonymous"
	}

	userBase64 := c.GetHeader(headerClinicCaller)
	if userBase64 == "" {
		return "anonymous"
	}

	userJson, err := base64.StdEncoding.DecodeString(userBase64)
	if err != nil {
		log.Error(c, "failed to decode session user", zap.Error(err))
		return "unknown"
	}
	var user SessionUser
	if err := json.Unmarshal(userJson, &user); err != nil {
		log.Error(c, "failed to unmarshal session user", zap.Error(err))
		return "unknown"
	}
	return user.Username
}

// StatusClass returns "2xx", "4xx", "5xx", or "other".
func StatusClass(code int) string {
	switch {
	case code >= 200 && code < 300:
		return "2xx"
	case code >= 400 && code < 500:
		return "4xx"
	case code >= 500 && code < 600:
		return "5xx"
	default:
		return "other"
	}
}

// NormalizeEmpty uses placeholder for Prometheus label safety.
func NormalizeEmpty(s string) string {
	if s == "" {
		return "unknown"
	}
	return s
}

// RecordDataProxySchemaRefresh records a low-cardinality schema maintenance outcome.
func RecordDataProxySchemaRefresh(provider, outcome string) {
	dataProxySchemaRefreshTotal.WithLabelValues(
		NormalizeEmpty(provider),
		NormalizeEmpty(outcome),
	).Inc()
}

// RecordDataProxy emits Prometheus series and a structured log line for Loki / dashboards.
func RecordDataProxy(c *gin.Context, start time.Time, rec *DataProxyRecord) {
	if rec == nil {
		rec = &DataProxyRecord{}
	}
	status := rec.HTTPStatus
	if status == 0 && c != nil && c.Writer != nil {
		status = c.Writer.Status()
	}
	if status == 0 {
		status = 200
	}
	sc := StatusClass(status)
	errCode := rec.ErrCode
	if errCode == "" && sc != "2xx" {
		errCode = "HTTP_" + strconv.Itoa(status)
	}
	if errCode == "" {
		errCode = "ok"
	}

	ep := NormalizeEmpty(rec.Endpoint)
	caller := NormalizeEmpty(rec.Caller)
	qtype := NormalizeEmpty(rec.QueryType)
	deployV2 := NormalizeEmpty(rec.DeployTypeV2)
	provider := NormalizeEmpty(rec.Provider)
	region := NormalizeEmpty(rec.Region)
	engine := NormalizeEmpty(rec.Engine)

	dataProxyRequestsTotal.WithLabelValues(
		ep, caller, qtype, deployV2, provider, region, engine, sc, errCode, strconv.Itoa(status),
	).Inc()

	dataProxyRequestDuration.WithLabelValues(
		ep, qtype, deployV2, provider, region, engine, sc,
	).Observe(time.Since(start).Seconds())

	ctx := context.Background()
	if c != nil && c.Request != nil {
		ctx = c.Request.Context()
	}
	log.Info(ctx, "dataproxy_http_request",
		zap.String("endpoint", ep),
		zap.String("caller", caller),
		zap.String("query_type", qtype),
		zap.String("deploy_type_v2", deployV2),
		zap.String("provider", provider),
		zap.String("region", region),
		zap.String("engine", engine),
		zap.String("status_class", sc),
		zap.Int("http_status", status),
		zap.String("error_code", errCode),
		zap.Float64("duration_seconds", time.Since(start).Seconds()),
	)
}
