package handlers

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/dto"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/observability"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/service"
)

// MetricsQueryHandler handles metrics query HTTP requests
// Provides Prometheus-compatible API endpoints
type MetricsQueryHandler struct {
	service *service.MetricsQueryService
}

// NewMetricsQueryHandler creates a new MetricsQueryHandler
func NewMetricsQueryHandler(svc *service.MetricsQueryService) *MetricsQueryHandler {
	return &MetricsQueryHandler{
		service: svc,
	}
}

// InstantQuery handles GET/POST /data-proxy/metrics/:cluster_id/api/v1/query
//
//	@Summary		Instant query
//	@Description	Evaluates an instant query at a single point in time (Prometheus-compatible)
//	@Tags			metrics
//	@Accept			json
//	@Accept			x-www-form-urlencoded
//	@Produce		json
//	@Param			cluster_id	path		string	true	"Cluster ID"
//	@Param			query		query		string	true	"PromQL expression"
//	@Param			time		query		string	false	"Evaluation timestamp (RFC3339 or Unix timestamp)"
//	@Param			timeout		query		string	false	"Evaluation timeout"
//	@Success		200			{object}	dto.PrometheusResponse
//	@Failure		400			{object}	dto.PrometheusResponse
//	@Failure		422			{object}	dto.PrometheusResponse
//	@Failure		500			{object}	dto.PrometheusResponse
//	@Router			/data-proxy/metrics/{cluster_id}/api/v1/query [get]
func (h *MetricsQueryHandler) InstantQuery(c *gin.Context) {
	h.instantQuery(c, "metrics_instant", c.Param("cluster_id"), nil)
}

func (h *MetricsQueryHandler) instantQuery(c *gin.Context, endpoint, clusterID string, target *clinic_internal_api.ClusterDataURLs) {
	obsStart := time.Now()
	rec := &observability.DataProxyRecord{
		Endpoint:  endpoint,
		Caller:    observability.CallerFromGin(c),
		QueryType: "metrics_instant",
	}
	applyMetricsTargetObservability(rec, target)
	defer func() { observability.RecordDataProxy(c, obsStart, rec) }()

	if clusterID == "" {
		c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "cluster_id is required",
		})
		return
	}

	// Get query parameter from query string or form
	query := c.Query("query")
	if query == "" {
		query = c.PostForm("query")
	}

	// Parse time parameter
	var queryTime time.Time
	timeStr := c.Query("time")
	if timeStr == "" {
		timeStr = c.PostForm("time")
	}
	if timeStr != "" {
		var err error
		queryTime, err = parseTime(timeStr)
		if err != nil {
			c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
				Status:    "error",
				ErrorType: "bad_data",
				Error:     "invalid time parameter: " + err.Error(),
			})
			return
		}
	}

	req := &dto.InstantQueryRequest{
		ClusterID: clusterID,
		Query:     query,
		Time:      queryTime,
		Timeout:   c.DefaultQuery("timeout", c.PostForm("timeout")),
	}

	var result *dto.PrometheusResponse
	if target == nil {
		result = h.service.InstantQuery(c.Request.Context(), req)
	} else {
		result = h.service.InstantQueryWithResolvedTarget(c.Request.Context(), req, target)
		sanitizeCanonicalMetricsResponse(result)
	}
	applyMetricsRouteObservability(rec, result)
	rec.ErrCode = errCodeFromPrometheusResponse(result)
	c.JSON(metricsHTTPStatusCode(result, target != nil), result)
}

// RangeQuery handles GET/POST /data-proxy/metrics/:cluster_id/api/v1/query_range
//
//	@Summary		Range query
//	@Description	Evaluates an expression query over a range of time (Prometheus-compatible)
//	@Tags			metrics
//	@Accept			json
//	@Accept			x-www-form-urlencoded
//	@Produce		json
//	@Param			cluster_id	path		string	true	"Cluster ID"
//	@Param			query		query		string	true	"PromQL expression"
//	@Param			start		query		string	true	"Start timestamp (RFC3339 or Unix timestamp)"
//	@Param			end			query		string	true	"End timestamp (RFC3339 or Unix timestamp)"
//	@Param			step		query		string	true	"Query resolution step width (duration format, e.g., 15s, 1m)"
//	@Param			timeout		query		string	false	"Evaluation timeout"
//	@Success		200			{object}	dto.PrometheusResponse
//	@Failure		400			{object}	dto.PrometheusResponse
//	@Failure		422			{object}	dto.PrometheusResponse
//	@Failure		500			{object}	dto.PrometheusResponse
//	@Router			/data-proxy/metrics/{cluster_id}/api/v1/query_range [get]
func (h *MetricsQueryHandler) RangeQuery(c *gin.Context) {
	h.rangeQuery(c, "metrics_range", c.Param("cluster_id"), nil)
}

func (h *MetricsQueryHandler) publicRangeQuery(c *gin.Context, clusterID string, target *clinic_internal_api.ClusterDataURLs) {
	h.rangeQueryWithForbiddenMetrics(c, "canonical_metrics_range", clusterID, target, true)
}

func (h *MetricsQueryHandler) rangeQuery(c *gin.Context, endpoint, clusterID string, target *clinic_internal_api.ClusterDataURLs) {
	h.rangeQueryWithForbiddenMetrics(c, endpoint, clusterID, target, false)
}

func (h *MetricsQueryHandler) rangeQueryWithForbiddenMetrics(c *gin.Context, endpoint, clusterID string, target *clinic_internal_api.ClusterDataURLs, filterForbidden bool) {
	obsStart := time.Now()
	rec := &observability.DataProxyRecord{
		Endpoint:  endpoint,
		Caller:    observability.CallerFromGin(c),
		QueryType: "metrics_range",
	}
	applyMetricsTargetObservability(rec, target)
	defer func() { observability.RecordDataProxy(c, obsStart, rec) }()

	if clusterID == "" {
		c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "cluster_id is required",
		})
		return
	}

	// Get query parameter
	query := c.Query("query")
	if query == "" {
		query = c.PostForm("query")
	}

	// Parse start time
	startStr := c.Query("start")
	if startStr == "" {
		startStr = c.PostForm("start")
	}
	start, err := parseTime(startStr)
	if err != nil {
		c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "invalid start parameter: " + err.Error(),
		})
		return
	}

	// Parse end time
	endStr := c.Query("end")
	if endStr == "" {
		endStr = c.PostForm("end")
	}
	end, err := parseTime(endStr)
	if err != nil {
		c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "invalid end parameter: " + err.Error(),
		})
		return
	}

	// Parse step
	stepStr := c.Query("step")
	if stepStr == "" {
		stepStr = c.PostForm("step")
	}
	step, err := parseDuration(stepStr)
	if err != nil {
		c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "invalid step parameter: " + err.Error(),
		})
		return
	}

	req := &dto.RangeQueryRequest{
		ClusterID: clusterID,
		Query:     query,
		Start:     start,
		End:       end,
		Step:      step,
		Timeout:   c.DefaultQuery("timeout", c.PostForm("timeout")),
	}

	var result *dto.PrometheusResponse
	if filterForbidden && forbiddenMetricsInPromQL(query) {
		result = emptyPrometheusQueryResponse("matrix")
	} else if target == nil {
		result = h.service.RangeQuery(c.Request.Context(), req)
	} else {
		result = h.service.RangeQueryWithResolvedTarget(c.Request.Context(), req, target)
		sanitizeCanonicalMetricsResponse(result)
	}
	applyMetricsRouteObservability(rec, result)
	rec.ErrCode = errCodeFromPrometheusResponse(result)
	c.JSON(metricsHTTPStatusCode(result, target != nil), result)
}

// Series handles GET/POST /data-proxy/metrics/:cluster_id/api/v1/series
//
//	@Summary		Series query
//	@Description	Returns the list of time series that match a certain label set (Prometheus-compatible)
//	@Tags			metrics
//	@Accept			json
//	@Accept			x-www-form-urlencoded
//	@Produce		json
//	@Param			cluster_id	path		string		true	"Cluster ID"
//	@Param			match[]		query		[]string	true	"Series selector (at least one required)"
//	@Param			start		query		string		false	"Start timestamp (RFC3339 or Unix timestamp)"
//	@Param			end			query		string		false	"End timestamp (RFC3339 or Unix timestamp)"
//	@Success		200			{object}	dto.PrometheusResponse
//	@Failure		400			{object}	dto.PrometheusResponse
//	@Failure		422			{object}	dto.PrometheusResponse
//	@Failure		500			{object}	dto.PrometheusResponse
//	@Router			/data-proxy/metrics/{cluster_id}/api/v1/series [get]
func (h *MetricsQueryHandler) Series(c *gin.Context) {
	h.series(c, "metrics_series", c.Param("cluster_id"), nil)
}

func (h *MetricsQueryHandler) publicSeries(c *gin.Context, clusterID string, target *clinic_internal_api.ClusterDataURLs) {
	h.seriesWithForbiddenMetrics(c, "canonical_metrics_series", clusterID, target, true)
}

func (h *MetricsQueryHandler) series(c *gin.Context, endpoint, clusterID string, target *clinic_internal_api.ClusterDataURLs) {
	h.seriesWithForbiddenMetrics(c, endpoint, clusterID, target, false)
}

func (h *MetricsQueryHandler) seriesWithForbiddenMetrics(c *gin.Context, endpoint, clusterID string, target *clinic_internal_api.ClusterDataURLs, filterForbidden bool) {
	obsStart := time.Now()
	rec := &observability.DataProxyRecord{
		Endpoint:  endpoint,
		Caller:    observability.CallerFromGin(c),
		QueryType: "metrics_series",
	}
	applyMetricsTargetObservability(rec, target)
	defer func() { observability.RecordDataProxy(c, obsStart, rec) }()

	if clusterID == "" {
		c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
			Status:    "error",
			ErrorType: "bad_data",
			Error:     "cluster_id is required",
		})
		return
	}

	// Get match[] parameters (can have multiple values)
	matchParams := c.QueryArray("match[]")
	if len(matchParams) == 0 {
		matchParams = c.PostFormArray("match[]")
	}

	// Parse start time (optional)
	var start time.Time
	startStr := c.Query("start")
	if startStr == "" {
		startStr = c.PostForm("start")
	}
	if startStr != "" {
		var err error
		start, err = parseTime(startStr)
		if err != nil {
			c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
				Status:    "error",
				ErrorType: "bad_data",
				Error:     "invalid start parameter: " + err.Error(),
			})
			return
		}
	}

	// Parse end time (optional)
	var end time.Time
	endStr := c.Query("end")
	if endStr == "" {
		endStr = c.PostForm("end")
	}
	if endStr != "" {
		var err error
		end, err = parseTime(endStr)
		if err != nil {
			c.JSON(http.StatusBadRequest, dto.PrometheusResponse{
				Status:    "error",
				ErrorType: "bad_data",
				Error:     "invalid end parameter: " + err.Error(),
			})
			return
		}
	}

	req := &dto.SeriesRequest{
		ClusterID: clusterID,
		Match:     matchParams,
		Start:     start,
		End:       end,
	}

	var result *dto.PrometheusResponse
	if filterForbidden && forbiddenMetricsInSeries(matchParams) {
		result = emptyPrometheusSeriesResponse()
	} else if target == nil {
		result = h.service.Series(c.Request.Context(), req)
	} else {
		result = h.service.SeriesWithResolvedTarget(c.Request.Context(), req, target)
		sanitizeCanonicalMetricsResponse(result)
	}
	applyMetricsRouteObservability(rec, result)
	rec.ErrCode = errCodeFromPrometheusResponse(result)
	c.JSON(metricsHTTPStatusCode(result, target != nil), result)
}

// parseTime parses a time string in RFC3339 format or Unix timestamp
func parseTime(s string) (time.Time, error) {
	if s == "" {
		return time.Time{}, nil
	}

	// Try RFC3339 first
	if t, err := time.Parse(time.RFC3339, s); err == nil {
		return t, nil
	}

	// Try RFC3339Nano
	if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
		return t, nil
	}

	// Try Unix timestamp (seconds)
	if ts, err := strconv.ParseFloat(s, 64); err == nil {
		sec := int64(ts)
		nsec := int64((ts - float64(sec)) * 1e9)
		return time.Unix(sec, nsec), nil
	}

	return time.Time{}, fmt.Errorf("cannot parse %q as RFC3339 or Unix timestamp", s)
}

// parseDuration parses a duration string (e.g., "15s", "1m", "1h")
func parseDuration(s string) (time.Duration, error) {
	if s == "" {
		return 0, nil
	}

	// Try Go duration format first
	if d, err := time.ParseDuration(s); err == nil {
		return d, nil
	}

	// Try parsing as seconds (float)
	if secs, err := strconv.ParseFloat(s, 64); err == nil {
		return time.Duration(secs * float64(time.Second)), nil
	}

	return 0, fmt.Errorf("cannot parse %q as duration", s)
}

// RegisterRoutes registers metrics query routes
// Routes are Prometheus-compatible:
// - GET/POST /data-proxy/metrics/:cluster_id/api/v1/query
// - GET/POST /data-proxy/metrics/:cluster_id/api/v1/query_range
// - GET/POST /data-proxy/metrics/:cluster_id/api/v1/series
func (h *MetricsQueryHandler) RegisterRoutes(r *gin.Engine) {
	group := r.Group("/data-proxy/metrics/:cluster_id/api/v1")
	{
		// Instant query - supports both GET and POST
		group.GET("/query", h.InstantQuery)
		group.POST("/query", h.InstantQuery)

		// Range query - supports both GET and POST
		group.GET("/query_range", h.RangeQuery)
		group.POST("/query_range", h.RangeQuery)

		// Series query - supports both GET and POST
		group.GET("/series", h.Series)
		group.POST("/series", h.Series)
	}
}

func errCodeFromPrometheusResponse(resp *dto.PrometheusResponse) string {
	if resp == nil || resp.Status == "success" {
		return ""
	}
	if resp.Code != "" {
		if resp.Code == "METRICS_UPSTREAM_UNPROCESSABLE" {
			return "PROMETHEUS_UPSTREAM_UNPROCESSABLE"
		}
		return "PROMETHEUS_" + resp.Code
	}
	if resp.ErrorType != "" {
		return "PROMETHEUS_" + strings.ToUpper(resp.ErrorType)
	}
	return "PROMETHEUS_ERROR"
}

func applyMetricsRouteObservability(rec *observability.DataProxyRecord, resp *dto.PrometheusResponse) {
	if rec == nil || resp == nil {
		return
	}
	rec.DeployTypeV2 = resp.DeployTypeV2
	rec.Provider = resp.Provider
	rec.Region = resp.Region
}

func applyMetricsTargetObservability(rec *observability.DataProxyRecord, target *clinic_internal_api.ClusterDataURLs) {
	if rec == nil || target == nil {
		return
	}
	rec.DeployTypeV2 = target.DeployTypeV2
	rec.Provider = strings.ToLower(strings.TrimSpace(target.Provider))
	rec.Region = strings.TrimSpace(target.Region)
}

// sanitizeCanonicalMetricsResponse keeps the Prometheus response envelope while
// preventing server-selected URLs, rewritten PromQL, and upstream bodies from
// crossing the canonical public boundary. Legacy handlers deliberately retain
// their established response text.
func sanitizeCanonicalMetricsResponse(resp *dto.PrometheusResponse) {
	if resp == nil || resp.Status != "error" {
		return
	}
	if resp.UpstreamStatus == http.StatusUnprocessableEntity && resp.Code != "" {
		return
	}
	switch resp.ErrorType {
	case "bad_data":
		// Parsing errors are derived solely from documented caller input.
		return
	case "unsupported_query":
		resp.Error = "metrics query is not supported"
	case "execution":
		resp.Error = "metrics upstream request failed"
	case "timeout":
		resp.Error = "metrics request timed out"
	case "canceled":
		resp.Error = "metrics request was canceled"
	case "not_found":
		resp.Error = "metrics target not found"
	default:
		resp.Error = "metrics query failed"
	}
}

func metricsHTTPStatusCode(resp *dto.PrometheusResponse, canonical bool) int {
	if resp != nil && resp.UpstreamStatus == http.StatusUnprocessableEntity {
		return http.StatusUnprocessableEntity
	}
	if !canonical || resp == nil || resp.Status == "success" {
		return getHTTPStatusCode(resp)
	}
	if resp.UpstreamStatus == http.StatusTooManyRequests {
		return http.StatusTooManyRequests
	}
	if resp.UpstreamStatus == http.StatusRequestTimeout || resp.UpstreamStatus == http.StatusGatewayTimeout {
		return http.StatusGatewayTimeout
	}
	if resp.UpstreamStatus != 0 {
		return http.StatusBadGateway
	}
	return getHTTPStatusCode(resp)
}

// getHTTPStatusCode returns the appropriate HTTP status code based on the Prometheus response
func getHTTPStatusCode(resp *dto.PrometheusResponse) int {
	if resp.Status == "success" {
		return http.StatusOK
	}

	// Map error types to HTTP status codes
	switch resp.ErrorType {
	case "bad_data":
		return http.StatusBadRequest
	case "unsupported_query":
		return http.StatusUnprocessableEntity
	case "execution":
		return http.StatusUnprocessableEntity
	case "internal":
		return http.StatusInternalServerError
	case "timeout":
		return http.StatusServiceUnavailable
	case "canceled":
		return http.StatusServiceUnavailable
	case "not_found":
		return http.StatusNotFound
	default:
		return http.StatusInternalServerError
	}
}
