package handlers

import (
	"context"
	stderrors "errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"unicode"

	"github.com/gin-gonic/gin"
	gozap "go.uber.org/zap"

	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
	apierrors "github.com/pingcap-inc/clinic/clinic_data_service/pkg/errors"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/observability"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/service"
)

const (
	maxTopRUQueryBytes       = 16 << 10
	maxTopRUValuesPerKey     = 16
	maxTopRUQueryValueLength = 4096
)

var (
	topRUSummaryQueryKeys = map[string]struct{}{
		"start": {}, "end": {}, "top": {}, "order_by": {}, "user": {}, "data_source": {}, "step": {},
	}
	topRUSummaryPlanQueryKeys = map[string]struct{}{
		"start": {}, "end": {}, "top": {}, "digest": {}, "order_by": {}, "user": {}, "data_source": {}, "step": {},
	}
	topRUUserStatsQueryKeys = map[string]struct{}{
		"start": {}, "end": {}, "top": {},
	}
)

type topRUQuerier interface {
	Query(context.Context, uint64, string, service.TopRUOperation, url.Values) (*service.TopRUResult, error)
}

// TopRUHandler exposes server-routed O11y Top RU data. Callers select only the
// logical org/cluster; placement headers are never used as routing input.
type TopRUHandler struct {
	service topRUQuerier
}

func NewTopRUHandler(topRUService topRUQuerier) *TopRUHandler {
	return &TopRUHandler{service: topRUService}
}

// Summary proxies O11y's Top RU SQL aggregation through Data Proxy.
//
// @Summary      Get Top RU summary
// @Tags         top-ru
// @Produce      json
// @Param        orgId path string true "TiDB Cloud/DBAAS org ID"
// @Param        clusterId path string true "Opaque TiDB Cloud cluster ID"
// @Param        start query int true "Inclusive window start in Unix seconds"
// @Param        end query int true "Inclusive window end in Unix seconds"
// @Param        top query int true "Top N SQL digests"
// @Param        order_by query string false "Ranking metric: ru, exec_count, or duration"
// @Param        user query string false "Omit for all users; user= selects empty/NULL user"
// @Param        data_source query string false "O11y data source, normally deltalake"
// @Param        step query int false "Trend bucket size in seconds"
// @Success      200 {object} map[string]interface{}
// @Failure      400 {object} apierrors.APIError
// @Failure      401 {object} apierrors.APIError
// @Failure      403 {object} apierrors.APIError
// @Failure      404 {object} apierrors.APIError
// @Failure      429 {object} apierrors.APIError
// @Failure      502 {object} apierrors.APIError
// @Failure      503 {object} apierrors.APIError
// @Failure      504 {object} apierrors.APIError
// @Security     BearerAuth
// @Router       /data-proxy/api/v1/orgs/{orgId}/clusters/{clusterId}/top-ru/summary [get]
func (h *TopRUHandler) Summary(c *gin.Context) {
	h.query(c, service.TopRUOperationSummary)
}

// SummaryPlan proxies O11y's Top RU SQL/plan aggregation through Data Proxy.
//
// @Summary      Get Top RU summary by plan
// @Tags         top-ru
// @Produce      json
// @Param        orgId path string true "TiDB Cloud/DBAAS org ID"
// @Param        clusterId path string true "Opaque TiDB Cloud cluster ID"
// @Param        start query int true "Inclusive window start in Unix seconds"
// @Param        end query int true "Inclusive window end in Unix seconds"
// @Param        top query int true "Top N SQL/plan groups"
// @Param        digest query string false "Filter to a single SQL digest"
// @Param        order_by query string false "Ranking metric: ru, exec_count, or duration"
// @Param        user query string false "Omit for all users; user= selects empty/NULL user"
// @Param        data_source query string false "O11y data source, normally deltalake"
// @Param        step query int false "Trend bucket size in seconds"
// @Success      200 {object} map[string]interface{}
// @Failure      400 {object} apierrors.APIError
// @Failure      401 {object} apierrors.APIError
// @Failure      403 {object} apierrors.APIError
// @Failure      404 {object} apierrors.APIError
// @Failure      429 {object} apierrors.APIError
// @Failure      502 {object} apierrors.APIError
// @Failure      503 {object} apierrors.APIError
// @Failure      504 {object} apierrors.APIError
// @Security     BearerAuth
// @Router       /data-proxy/api/v1/orgs/{orgId}/clusters/{clusterId}/top-ru/summary-plan [get]
func (h *TopRUHandler) SummaryPlan(c *gin.Context) {
	h.query(c, service.TopRUOperationSummaryPlan)
}

// UserStats proxies O11y's Top RU per-user aggregation through Data Proxy.
//
// @Summary      Get Top RU user statistics
// @Tags         top-ru
// @Produce      json
// @Param        orgId path string true "TiDB Cloud/DBAAS org ID"
// @Param        clusterId path string true "Opaque TiDB Cloud cluster ID"
// @Param        start query int true "Inclusive window start in Unix seconds"
// @Param        end query int true "Inclusive window end in Unix seconds"
// @Param        top query int false "Top N users; O11y default is 20"
// @Success      200 {object} map[string]interface{}
// @Failure      400 {object} apierrors.APIError
// @Failure      401 {object} apierrors.APIError
// @Failure      403 {object} apierrors.APIError
// @Failure      404 {object} apierrors.APIError
// @Failure      429 {object} apierrors.APIError
// @Failure      502 {object} apierrors.APIError
// @Failure      503 {object} apierrors.APIError
// @Failure      504 {object} apierrors.APIError
// @Security     BearerAuth
// @Router       /data-proxy/api/v1/orgs/{orgId}/clusters/{clusterId}/top-ru/user-stats [get]
func (h *TopRUHandler) UserStats(c *gin.Context) {
	h.query(c, service.TopRUOperationUserStats)
}

func (h *TopRUHandler) query(c *gin.Context, operation service.TopRUOperation) {
	endpoint := "topru_" + string(operation)
	start, rec := beginMetadataRequest(c, endpoint)
	rec.QueryType = "topru"
	rec.Engine = "o11y"
	defer func() { observability.RecordDataProxy(c, start, rec) }()

	orgID, ok := parseOrgID(c, rec)
	if !ok {
		return
	}
	clusterID := c.Param("clusterId")
	if err := validateResourceID(clusterID); err != nil {
		rec.ErrCode = "INVALID_CLUSTER_ID"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", err.Error(), nil)
		return
	}
	query, err := topRUQuery(c, operation)
	if err != nil {
		rec.ErrCode = "INVALID_TOPRU_QUERY"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", err.Error(), nil)
		return
	}
	if h.service == nil {
		rec.ErrCode = "TOPRU_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "Top RU service is temporarily unavailable", nil)
		return
	}

	result, err := h.service.Query(c.Request.Context(), orgID, clusterID, operation, query)
	if err != nil {
		h.writeTopRUError(c, rec, err, clusterID)
		return
	}
	if result == nil {
		rec.ErrCode = "TOPRU_INVALID_RESPONSE"
		writeResourceError(c, http.StatusBadGateway, "BadGateway", "Top RU returned an invalid response", nil)
		return
	}
	rec.Provider = result.Provider
	rec.Region = result.Region
	rec.DeployTypeV2 = result.DeployTypeV2
	c.Data(http.StatusOK, "application/json; charset=utf-8", result.Body)
}

func (h *TopRUHandler) RegisterRoutes(r *gin.Engine) {
	group := r.Group("/data-proxy/api/v1")
	group.GET("/orgs/:orgId/clusters/:clusterId/top-ru/summary", h.Summary)
	group.GET("/orgs/:orgId/clusters/:clusterId/top-ru/summary-plan", h.SummaryPlan)
	group.GET("/orgs/:orgId/clusters/:clusterId/top-ru/user-stats", h.UserStats)
}

func topRUQuery(c *gin.Context, operation service.TopRUOperation) (url.Values, error) {
	if len(c.Request.URL.RawQuery) > maxTopRUQueryBytes {
		return nil, fmt.Errorf("query must not exceed %d bytes", maxTopRUQueryBytes)
	}
	parsed, err := url.ParseQuery(c.Request.URL.RawQuery)
	if err != nil {
		return nil, fmt.Errorf("query contains invalid escaping: %w", err)
	}
	allowed := topRUSummaryQueryKeys
	switch operation {
	case service.TopRUOperationSummaryPlan:
		allowed = topRUSummaryPlanQueryKeys
	case service.TopRUOperationUserStats:
		allowed = topRUUserStatsQueryKeys
	}
	result := make(url.Values, len(parsed))
	for key, values := range parsed {
		if _, ok := allowed[key]; !ok {
			return nil, fmt.Errorf("unsupported Top RU query parameter %q", key)
		}
		if len(values) > maxTopRUValuesPerKey {
			return nil, fmt.Errorf("%s must not be repeated more than %d times", key, maxTopRUValuesPerKey)
		}
		for _, value := range values {
			if len(value) > maxTopRUQueryValueLength {
				return nil, fmt.Errorf("%s must not exceed %d bytes", key, maxTopRUQueryValueLength)
			}
			for _, r := range value {
				if unicode.IsControl(r) {
					return nil, fmt.Errorf("%s must not contain control characters", key)
				}
			}
		}
		result[key] = append([]string(nil), values...)
	}
	return result, nil
}

func (h *TopRUHandler) writeTopRUError(c *gin.Context, rec *observability.DataProxyRecord, err error, clusterID string) {
	ctx := c.Request.Context()
	switch {
	case stderrors.Is(err, context.Canceled):
		rec.ErrCode = "TOPRU_CANCELED"
		writeResourceError(c, clientClosedStatus, "Canceled", "Top RU request was canceled", nil)
		return
	case stderrors.Is(err, context.DeadlineExceeded):
		rec.ErrCode = "TOPRU_TIMEOUT"
		writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "Top RU request timed out", nil)
		return
	case stderrors.Is(err, service.ErrTopRUUnavailable):
		rec.ErrCode = "TOPRU_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "Top RU service is temporarily unavailable", nil)
		return
	case stderrors.Is(err, service.ErrInvalidTopRUTarget), stderrors.Is(err, service.ErrInvalidTopRUResponse):
		rec.ErrCode = "TOPRU_INVALID_UPSTREAM"
		writeResourceError(c, http.StatusBadGateway, "BadGateway", "Top RU returned an invalid response", nil)
		return
	}

	var networkError net.Error
	if stderrors.As(err, &networkError) {
		if networkError.Timeout() {
			rec.ErrCode = "TOPRU_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "Top RU request timed out", nil)
			return
		}
		rec.ErrCode = "TOPRU_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "Top RU service is temporarily unavailable", nil)
		return
	}

	var clinicError *clinic_internal_api.UpstreamError
	if stderrors.As(err, &clinicError) {
		switch clinicError.StatusCode {
		case http.StatusBadRequest:
			rec.ErrCode = "TOPRU_UNSUPPORTED"
			writeResourceError(c, http.StatusBadRequest, "InvalidArgument", "Top RU is not supported for this cluster", nil)
		case http.StatusNotFound:
			rec.ErrCode = "CLUSTER_NOT_FOUND"
			writeResourceError(c, http.StatusNotFound, "NotFound", "cluster not found", nil)
		case http.StatusRequestTimeout, http.StatusGatewayTimeout:
			rec.ErrCode = "TOPRU_TARGET_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "Top RU target resolution timed out", nil)
		case http.StatusServiceUnavailable:
			rec.ErrCode = "TOPRU_TARGET_UNAVAILABLE"
			writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "Top RU target is temporarily unavailable", nil)
		default:
			rec.ErrCode = "TOPRU_TARGET_ERROR"
			writeResourceError(c, http.StatusBadGateway, "BadGateway", "failed to resolve Top RU target", nil)
		}
		return
	}

	var upstreamError *service.TopRUUpstreamError
	if stderrors.As(err, &upstreamError) {
		switch upstreamError.StatusCode {
		case http.StatusBadRequest:
			rec.ErrCode = "INVALID_TOPRU_QUERY"
			writeResourceError(c, http.StatusBadRequest, "InvalidArgument", "invalid Top RU query", nil)
		case http.StatusTooManyRequests:
			rec.ErrCode = "TOPRU_RATE_LIMITED"
			writeResourceError(c, http.StatusTooManyRequests, "ResourceExhausted", "Top RU rate limit exceeded", nil)
		case http.StatusRequestTimeout, http.StatusGatewayTimeout:
			rec.ErrCode = "TOPRU_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "Top RU request timed out", nil)
		case http.StatusServiceUnavailable:
			rec.ErrCode = "TOPRU_UNAVAILABLE"
			writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "Top RU service is temporarily unavailable", nil)
		default:
			rec.ErrCode = "TOPRU_UPSTREAM_ERROR"
			writeResourceError(c, http.StatusBadGateway, "BadGateway", "Top RU upstream request failed", nil)
		}
		return
	}

	rec.ErrCode = "TOPRU_INTERNAL_ERROR"
	log.Error(ctx, "Top RU request failed", gozap.String("cluster_id", clusterID), gozap.String("error_type", fmt.Sprintf("%T", err)))
	writeResourceError(c, http.StatusBadGateway, "BadGateway", "Top RU request failed", nil)
}

// Keep the qualified APIError import visible to the OpenAPI comment parser.
var _ apierrors.APIError
