package handlers

import (
	"context"
	stderrors "errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"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 (
	maxBYOCStatementsQueryBytes       = 16 << 10
	maxBYOCStatementsTimeRangeSeconds = 3*24*60*60 + 5*60
	maxBYOCStatementsUnixSeconds      = int64(253402300799)
	maxBYOCStatementsFieldsBytes      = 4096
	maxBYOCStatementsTextBytes        = 4096
	maxBYOCStatementsValueBytes       = 256
	maxBYOCStatementsCollectionValues = 64
)

var (
	byocStatementsFieldNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]{0,127}$`)
	byocStatementsDigestPattern    = regexp.MustCompile(`^[A-Fa-f0-9]{64}$`)

	byocStatementsAllowedFields = byocStatementsStringSet(`
		summary_begin_time summary_end_time digest_text digest exec_count stmt_type
		sum_errors sum_warnings sum_latency max_latency min_latency avg_latency
		avg_parse_latency max_parse_latency avg_compile_latency max_compile_latency
		sum_cop_task_num max_cop_process_time max_cop_wait_time avg_process_time
		max_process_time avg_wait_time max_wait_time avg_backoff_time max_backoff_time
		avg_total_keys max_total_keys avg_processed_keys max_processed_keys
		avg_prewrite_time max_prewrite_time avg_commit_time max_commit_time
		avg_get_commit_ts_time max_get_commit_ts_time avg_commit_backoff_time
		max_commit_backoff_time avg_resolve_lock_time max_resolve_lock_time
		avg_local_latch_wait_time max_local_latch_wait_time avg_write_keys max_write_keys
		avg_write_size max_write_size avg_prewrite_regions max_prewrite_regions
		avg_txn_retry max_txn_retry sum_backoff_times avg_mem max_mem avg_disk max_disk
		avg_affected_rows first_seen last_seen sample_user user query_sample_text
		prev_sample_text schema_name table_names index_names plan_count plan plan_digest
		binary_plan plan_hint plan_in_binding max_rocksdb_delete_skipped_count
		avg_rocksdb_delete_skipped_count max_rocksdb_key_skipped_count
		avg_rocksdb_key_skipped_count max_rocksdb_block_cache_hit_count
		avg_rocksdb_block_cache_hit_count max_rocksdb_block_read_count
		avg_rocksdb_block_read_count max_rocksdb_block_read_byte
		avg_rocksdb_block_read_byte sum_ru sum_ru_v2 avg_ru_v2 max_ru_v2 sum_rru
		sum_wru sum_ru_wait_duration avg_ru avg_rru avg_wru max_rru max_wru
		max_ru_wait_duration
	`)
	byocStatementsAllowedTypes = byocStatementsStringSet(`
		AlterTable AnalyzeTable Begin Change Commit CompactTable CreateDatabase CreateIndex
		CreateTable CreateView CreateUser Delete DropDatabase DropIndex DropView DropTable
		DescTable ExplainAnalyzeSQL ExplainSQL Replace Insert LoadData Rollback Select Set Show
		TruncateTable Update Grant Revoke Deallocate Execute Prepare Use CreateBinding
		IndexAdvise DropBinding Trace Shutdown Savepoint other
	`)

	byocStatementsListKeys = map[string]struct{}{
		"begin_time": {}, "end_time": {}, "schemas": {}, "stmt_types": {}, "text": {}, "fields": {},
	}
	byocStatementsPlansKeys = map[string]struct{}{
		"begin_time": {}, "end_time": {}, "digest": {}, "schema_name": {},
	}
	byocStatementsPlanDetailKeys = map[string]struct{}{
		"begin_time": {}, "end_time": {}, "digest": {}, "plans": {}, "schema_name": {},
	}
	byocStatementsServerRoutingHeaders = map[string]struct{}{
		"xenv": {}, "xbiztype": {}, "xorgid": {}, "xregionid": {}, "xapplicationid": {},
		"xdeploytype": {}, "xdeploytypev2": {}, "xo11yclusterid": {},
	}

	errBYOCStatementsParametersTooLarge = stderrors.New("query parameters exceed the allowed size")
)

type byocStatementsQuerier interface {
	Query(context.Context, uint64, string, service.BYOCStatementsOperation, url.Values) (*service.BYOCStatementsResult, error)
}

// BYOCStatementsHandler exposes live BYOC SQL Statements from customer-side
// O11y. It never uses Data Proxy SQL, object storage, or warehouse tables.
type BYOCStatementsHandler struct {
	service byocStatementsQuerier
}

func NewBYOCStatementsHandler(svc byocStatementsQuerier) *BYOCStatementsHandler {
	return &BYOCStatementsHandler{service: svc}
}

// AvailableFields returns the fields supported by the resolved BYOC O11y target.
//
//	@Summary		List BYOC SQL Statement fields
//	@ID			canonical-byoc-statements-available-fields
//	@Tags			data-proxy-statements
//	@Produce		json
//	@Param			orgId		path		string	true	"TiDB Cloud/DBAAS org ID"
//	@Param			clusterId	path		string	true	"Logical BYOC or physical byoc-host cluster ID"
//	@Success		200			{array}		string
//	@Failure		400			{object}	apierrors.APIError
//	@Failure		401			{object}	apierrors.APIError
//	@Failure		403			{object}	apierrors.APIError
//	@Failure		404			{object}	apierrors.APIError
//	@Failure		413			{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}/statements/available-fields [get]
func (h *BYOCStatementsHandler) AvailableFields(c *gin.Context) {
	h.handle(c, "canonical_byoc_statements_available_fields", service.BYOCStatementsOperationAvailableFields, noBYOCStatementsParams)
}

// StatementTypes returns the statement kinds supported by the resolved BYOC O11y target.
//
//	@Summary		List BYOC SQL Statement types
//	@ID			canonical-byoc-statements-types
//	@Tags			data-proxy-statements
//	@Produce		json
//	@Param			orgId		path		string	true	"TiDB Cloud/DBAAS org ID"
//	@Param			clusterId	path		string	true	"Logical BYOC or physical byoc-host cluster ID"
//	@Success		200			{array}		string
//	@Failure		400			{object}	apierrors.APIError
//	@Failure		401			{object}	apierrors.APIError
//	@Failure		403			{object}	apierrors.APIError
//	@Failure		404			{object}	apierrors.APIError
//	@Failure		413			{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}/statements/statement-types [get]
func (h *BYOCStatementsHandler) StatementTypes(c *gin.Context) {
	h.handle(c, "canonical_byoc_statements_types", service.BYOCStatementsOperationTypes, noBYOCStatementsParams)
}

// List returns aggregated SQL Statements from customer-side O11y.
//
//	@Summary		List BYOC SQL Statements
//	@ID			canonical-byoc-statements-list
//	@Tags			data-proxy-statements
//	@Produce		json
//	@Param			orgId		path		string		true	"TiDB Cloud/DBAAS org ID"
//	@Param			clusterId	path		string		true	"Logical BYOC or physical byoc-host cluster ID"
//	@Param			begin_time	query		int			true	"Window start in Unix seconds; range must not exceed 259500 seconds"	minimum(0)	maximum(253402300799)
//	@Param			end_time	query		int			true	"Window end in Unix seconds; range must not exceed 259500 seconds"	minimum(0)	maximum(253402300799)
//	@Param			schemas		query		[]string	false	"Schema filters; repeat the parameter, at most 64 values"	collectionFormat(multi)
//	@Param			stmt_types	query		[]string	false	"Statement-type filters; repeat the parameter, at most 64 values"	collectionFormat(multi)
//	@Param			text		query		string		false	"SQL text filter, at most 4096 bytes"	maxLength(4096)
//	@Param			fields		query		string		false	"Comma-separated result field names, at most 4096 bytes"	maxLength(4096)
//	@Success		200			{array}		map[string]interface{}
//	@Failure		400			{object}	apierrors.APIError
//	@Failure		401			{object}	apierrors.APIError
//	@Failure		403			{object}	apierrors.APIError
//	@Failure		404			{object}	apierrors.APIError
//	@Failure		413			{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}/statements [get]
func (h *BYOCStatementsHandler) List(c *gin.Context) {
	h.handle(c, "canonical_byoc_statements_list", service.BYOCStatementsOperationList, canonicalBYOCStatementsList)
}

// Plans returns plan aggregates for one SQL digest.
//
//	@Summary		List BYOC SQL Statement plans
//	@ID			canonical-byoc-statements-plans
//	@Tags			data-proxy-statements
//	@Produce		json
//	@Param			orgId		path		string	true	"TiDB Cloud/DBAAS org ID"
//	@Param			clusterId	path		string	true	"Logical BYOC or physical byoc-host cluster ID"
//	@Param			begin_time	query		int		true	"Window start in Unix seconds; range must not exceed 259500 seconds"	minimum(0)	maximum(253402300799)
//	@Param			end_time	query		int		true	"Window end in Unix seconds; range must not exceed 259500 seconds"	minimum(0)	maximum(253402300799)
//	@Param			digest		query		string	true	"SQL digest, exactly 64 hexadecimal characters"	minLength(64)	maxLength(64)
//	@Param			schema_name	query		string	false	"Schema filter, at most 256 bytes"	maxLength(256)
//	@Success		200			{array}		map[string]interface{}
//	@Failure		400			{object}	apierrors.APIError
//	@Failure		401			{object}	apierrors.APIError
//	@Failure		403			{object}	apierrors.APIError
//	@Failure		404			{object}	apierrors.APIError
//	@Failure		413			{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}/statements/plans [get]
func (h *BYOCStatementsHandler) Plans(c *gin.Context) {
	h.handle(c, "canonical_byoc_statements_plans", service.BYOCStatementsOperationPlans, canonicalBYOCStatementsPlans)
}

// PlanDetail returns the aggregate detail for one SQL digest and optional plan digests.
//
//	@Summary		Get BYOC SQL Statement plan detail
//	@ID			canonical-byoc-statements-plan-detail
//	@Tags			data-proxy-statements
//	@Produce		json
//	@Param			orgId		path		string		true	"TiDB Cloud/DBAAS org ID"
//	@Param			clusterId	path		string		true	"Logical BYOC or physical byoc-host cluster ID"
//	@Param			begin_time	query		int			true	"Window start in Unix seconds; range must not exceed 259500 seconds"	minimum(0)	maximum(253402300799)
//	@Param			end_time	query		int			true	"Window end in Unix seconds; range must not exceed 259500 seconds"	minimum(0)	maximum(253402300799)
//	@Param			digest		query		string		true	"SQL digest, exactly 64 hexadecimal characters"	minLength(64)	maxLength(64)
//	@Param			plans		query		[]string	false	"Optional plan digests; omit for all plans, otherwise repeat the parameter (at most 64)"	collectionFormat(multi)
//	@Param			schema_name	query		string		false	"Schema filter, at most 256 bytes"	maxLength(256)
//	@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		413			{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}/statements/plan-detail [get]
func (h *BYOCStatementsHandler) PlanDetail(c *gin.Context) {
	h.handle(c, "canonical_byoc_statements_plan_detail", service.BYOCStatementsOperationPlanDetail, canonicalBYOCStatementsPlanDetail)
}

type byocStatementsParamsParser func(*gin.Context) (url.Values, error)

func (h *BYOCStatementsHandler) handle(
	c *gin.Context,
	endpoint string,
	operation service.BYOCStatementsOperation,
	parseParams byocStatementsParamsParser,
) {
	start, rec := beginCanonicalDataProxyRequest(c, endpoint)
	rec.QueryType = "statements"
	rec.Engine = "o11y"
	defer func() { observability.RecordDataProxy(c, start, rec) }()

	if len(c.Request.URL.RawQuery) > maxBYOCStatementsQueryBytes {
		rec.ErrCode = "INVALID_STATEMENTS_PARAMETERS"
		writeResourceError(c, http.StatusRequestEntityTooLarge, "PayloadTooLarge", errBYOCStatementsParametersTooLarge.Error(), nil)
		return
	}
	if !requireNoCanonicalBody(c, rec) || !rejectCanonicalPlacement(c, rec) || !rejectBYOCStatementsRoutingHeaders(c, rec) {
		return
	}
	orgID, clusterID, ok := parseCanonicalTelemetryScope(c, rec)
	if !ok {
		return
	}
	query, err := parseParams(c)
	if err != nil {
		rec.ErrCode = "INVALID_STATEMENTS_PARAMETERS"
		status := http.StatusBadRequest
		code := "InvalidArgument"
		if stderrors.Is(err, errBYOCStatementsParametersTooLarge) {
			status = http.StatusRequestEntityTooLarge
			code = "PayloadTooLarge"
		}
		writeResourceError(c, status, code, err.Error(), nil)
		return
	}
	if h == nil || h.service == nil {
		rec.ErrCode = "STATEMENTS_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "BYOC Statements service is temporarily unavailable", nil)
		return
	}

	result, err := h.service.Query(c.Request.Context(), orgID, clusterID, operation, query)
	if err != nil {
		h.writeError(c, rec, err, operation, clusterID)
		return
	}
	if result == nil {
		rec.ErrCode = "STATEMENTS_INVALID_RESPONSE"
		writeResourceError(c, http.StatusBadGateway, "BadGateway", "BYOC Statements 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 *BYOCStatementsHandler) RegisterRoutes(r *gin.Engine) {
	group := r.Group("/data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/statements")
	group.GET("", h.List)
	group.GET("/available-fields", h.AvailableFields)
	group.GET("/statement-types", h.StatementTypes)
	group.GET("/plans", h.Plans)
	group.GET("/plan-detail", h.PlanDetail)
}

func rejectBYOCStatementsRoutingHeaders(c *gin.Context, rec *observability.DataProxyRecord) bool {
	for key := range c.Request.Header {
		if _, blocked := byocStatementsServerRoutingHeaders[normalizeCanonicalRoutingKey(key)]; blocked {
			return writeCanonicalPlacementError(c, rec)
		}
	}
	return true
}

func parseBYOCStatementsValues(c *gin.Context, allowed map[string]struct{}) (url.Values, error) {
	if len(c.Request.URL.RawQuery) > maxBYOCStatementsQueryBytes {
		return nil, errBYOCStatementsParametersTooLarge
	}
	values, err := url.ParseQuery(c.Request.URL.RawQuery)
	if err != nil {
		return nil, fmt.Errorf("query parameters are malformed")
	}
	for key := range values {
		if _, ok := allowed[key]; !ok {
			return nil, fmt.Errorf("unsupported Statements parameter %q", key)
		}
	}
	return values, nil
}

func noBYOCStatementsParams(c *gin.Context) (url.Values, error) {
	values, err := parseBYOCStatementsValues(c, map[string]struct{}{})
	if err != nil {
		return nil, err
	}
	if len(values) != 0 {
		return nil, fmt.Errorf("query parameters are not supported")
	}
	return make(url.Values), nil
}

func canonicalBYOCStatementsList(c *gin.Context) (url.Values, error) {
	values, err := parseBYOCStatementsValues(c, byocStatementsListKeys)
	if err != nil {
		return nil, err
	}
	if err := requireSingleBYOCStatementsValues(values, "begin_time", "end_time", "text", "fields"); err != nil {
		return nil, err
	}
	if err := validateBYOCStatementsWindow(values); err != nil {
		return nil, err
	}
	if err := validateBYOCStatementsCollection(values, "schemas", false); err != nil {
		return nil, err
	}
	if err := validateBYOCStatementsCollection(values, "stmt_types", false); err != nil {
		return nil, err
	}
	if values.Has("text") {
		if err := validateBYOCStatementsText("text", values.Get("text"), maxBYOCStatementsTextBytes, true); err != nil {
			return nil, err
		}
	}

	result := cloneBYOCStatementsValues(values)
	if values.Has("fields") {
		fields, err := canonicalBYOCStatementsFields(values.Get("fields"))
		if err != nil {
			return nil, err
		}
		result.Set("fields", fields)
	}
	return result, nil
}

func canonicalBYOCStatementsPlans(c *gin.Context) (url.Values, error) {
	values, err := parseBYOCStatementsValues(c, byocStatementsPlansKeys)
	if err != nil {
		return nil, err
	}
	if err := requireSingleBYOCStatementsValues(values, "begin_time", "end_time", "digest", "schema_name"); err != nil {
		return nil, err
	}
	if err := validateBYOCStatementsWindow(values); err != nil {
		return nil, err
	}
	if err := validateRequiredBYOCStatementsDigest(values); err != nil {
		return nil, err
	}
	if values.Has("schema_name") {
		if err := validateBYOCStatementsText("schema_name", values.Get("schema_name"), maxBYOCStatementsValueBytes, true); err != nil {
			return nil, err
		}
	}
	return cloneBYOCStatementsValues(values), nil
}

func canonicalBYOCStatementsPlanDetail(c *gin.Context) (url.Values, error) {
	values, err := parseBYOCStatementsValues(c, byocStatementsPlanDetailKeys)
	if err != nil {
		return nil, err
	}
	if err := requireSingleBYOCStatementsValues(values, "begin_time", "end_time", "digest", "schema_name"); err != nil {
		return nil, err
	}
	if err := validateBYOCStatementsWindow(values); err != nil {
		return nil, err
	}
	if err := validateRequiredBYOCStatementsDigest(values); err != nil {
		return nil, err
	}
	if err := validateBYOCStatementsCollection(values, "plans", true); err != nil {
		return nil, err
	}
	if values.Has("schema_name") {
		if err := validateBYOCStatementsText("schema_name", values.Get("schema_name"), maxBYOCStatementsValueBytes, true); err != nil {
			return nil, err
		}
	}
	return cloneBYOCStatementsValues(values), nil
}

func requireSingleBYOCStatementsValues(values url.Values, keys ...string) error {
	for _, key := range keys {
		if len(values[key]) > 1 {
			return fmt.Errorf("%s must not be repeated", key)
		}
	}
	return nil
}

func validateBYOCStatementsWindow(values url.Values) error {
	begin, err := parseRequiredBYOCStatementsTimestamp(values, "begin_time")
	if err != nil {
		return err
	}
	end, err := parseRequiredBYOCStatementsTimestamp(values, "end_time")
	if err != nil {
		return err
	}
	if end < begin {
		return fmt.Errorf("end_time must be greater than or equal to begin_time")
	}
	if end-begin > maxBYOCStatementsTimeRangeSeconds {
		return fmt.Errorf("time range must not exceed %d seconds", maxBYOCStatementsTimeRangeSeconds)
	}
	return nil
}

func parseRequiredBYOCStatementsTimestamp(values url.Values, key string) (int64, error) {
	if !values.Has(key) || values.Get(key) == "" {
		return 0, fmt.Errorf("%s is required", key)
	}
	value, err := strconv.ParseInt(values.Get(key), 10, 64)
	if err != nil || value < 0 || value > maxBYOCStatementsUnixSeconds {
		return 0, fmt.Errorf("%s must be a non-negative Unix timestamp not greater than %d", key, maxBYOCStatementsUnixSeconds)
	}
	return value, nil
}

func validateRequiredBYOCStatementsDigest(values url.Values) error {
	if !values.Has("digest") || values.Get("digest") == "" {
		return fmt.Errorf("digest is required")
	}
	digest := values.Get("digest")
	if err := validateBYOCStatementsText("digest", digest, maxBYOCStatementsValueBytes, false); err != nil {
		return err
	}
	if !byocStatementsDigestPattern.MatchString(digest) {
		return fmt.Errorf("digest must be a 64-character hexadecimal SQL digest")
	}
	return nil
}

func validateBYOCStatementsCollection(values url.Values, key string, allowNA bool) error {
	items := values[key]
	if len(items) > maxBYOCStatementsCollectionValues {
		return fmt.Errorf("%s must not contain more than %d values", key, maxBYOCStatementsCollectionValues)
	}
	for _, item := range items {
		if err := validateBYOCStatementsText(key, item, maxBYOCStatementsValueBytes, false); err != nil {
			return err
		}
		switch key {
		case "stmt_types":
			if _, ok := byocStatementsAllowedTypes[item]; !ok {
				return fmt.Errorf("stmt_types contains an unsupported statement type")
			}
		case "plans":
			if item != "N/A" && !byocStatementsDigestPattern.MatchString(item) {
				return fmt.Errorf("plans values must be 64-character hexadecimal plan digests or N/A")
			}
			if item == "N/A" && !allowNA {
				return fmt.Errorf("plans contains an invalid value")
			}
		}
	}
	return nil
}

func canonicalBYOCStatementsFields(raw string) (string, error) {
	if raw == "" {
		return "", fmt.Errorf("fields must not be empty")
	}
	if len(raw) > maxBYOCStatementsFieldsBytes {
		return "", fmt.Errorf("fields must not exceed %d bytes", maxBYOCStatementsFieldsBytes)
	}
	parts := strings.Split(raw, ",")
	if len(parts) > 128 {
		return "", fmt.Errorf("fields must not contain more than 128 field names")
	}
	seen := make(map[string]struct{}, len(parts))
	result := make([]string, 0, len(parts))
	for _, part := range parts {
		part = strings.ToLower(strings.TrimSpace(part))
		if !byocStatementsFieldNamePattern.MatchString(part) {
			return "", fmt.Errorf("fields contains an invalid field name")
		}
		if _, ok := byocStatementsAllowedFields[part]; !ok {
			return "", fmt.Errorf("fields contains an unsupported field name")
		}
		if _, ok := seen[part]; ok {
			return "", fmt.Errorf("fields contains a duplicate field name")
		}
		seen[part] = struct{}{}
		result = append(result, part)
	}
	return strings.Join(result, ","), nil
}

func validateBYOCStatementsText(key, value string, maxBytes int, allowEmpty bool) error {
	if !allowEmpty && value == "" {
		return fmt.Errorf("%s must not be empty", key)
	}
	if len(value) > maxBytes {
		return fmt.Errorf("%s must not exceed %d bytes", key, maxBytes)
	}
	if !utf8.ValidString(value) {
		return fmt.Errorf("%s must be valid UTF-8", key)
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return fmt.Errorf("%s must not contain control characters", key)
		}
	}
	return nil
}

func byocStatementsStringSet(values string) map[string]struct{} {
	items := strings.Fields(values)
	result := make(map[string]struct{}, len(items))
	for _, item := range items {
		result[item] = struct{}{}
	}
	return result
}

func cloneBYOCStatementsValues(values url.Values) url.Values {
	result := make(url.Values, len(values))
	for key, items := range values {
		result[key] = append([]string(nil), items...)
	}
	return result
}

func (h *BYOCStatementsHandler) writeError(
	c *gin.Context,
	rec *observability.DataProxyRecord,
	err error,
	operation service.BYOCStatementsOperation,
	clusterID string,
) {
	switch {
	case stderrors.Is(err, context.Canceled):
		rec.ErrCode = "STATEMENTS_CANCELED"
		writeResourceError(c, clientClosedStatus, "Canceled", "BYOC Statements request was canceled", nil)
		return
	case stderrors.Is(err, context.DeadlineExceeded):
		rec.ErrCode = "STATEMENTS_TIMEOUT"
		writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "BYOC Statements request timed out", nil)
		return
	case stderrors.Is(err, service.ErrBYOCStatementsUnavailable):
		rec.ErrCode = "STATEMENTS_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "BYOC Statements service is temporarily unavailable", nil)
		return
	case stderrors.Is(err, service.ErrInvalidBYOCStatementsTarget), stderrors.Is(err, service.ErrInvalidBYOCStatementsResponse):
		rec.ErrCode = "STATEMENTS_INVALID_UPSTREAM"
		writeResourceError(c, http.StatusBadGateway, "BadGateway", "BYOC Statements returned an invalid response", nil)
		return
	}

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

	var targetError *clinic_internal_api.UpstreamError
	if stderrors.As(err, &targetError) {
		switch targetError.StatusCode {
		case http.StatusBadRequest:
			rec.ErrCode = "STATEMENTS_UNSUPPORTED"
			writeResourceError(c, http.StatusBadRequest, "InvalidArgument", "BYOC Statements are 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 = "STATEMENTS_TARGET_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "BYOC Statements target resolution timed out", nil)
		case http.StatusTooManyRequests:
			rec.ErrCode = "STATEMENTS_TARGET_RATE_LIMITED"
			writeResourceError(c, http.StatusTooManyRequests, "ResourceExhausted", "BYOC Statements target rate limit exceeded", nil)
		case http.StatusServiceUnavailable:
			rec.ErrCode = "STATEMENTS_TARGET_UNAVAILABLE"
			writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "BYOC Statements target is temporarily unavailable", nil)
		default:
			rec.ErrCode = "STATEMENTS_TARGET_ERROR"
			writeResourceError(c, http.StatusBadGateway, "BadGateway", "failed to resolve BYOC Statements target", nil)
		}
		return
	}

	var upstreamError *service.BYOCStatementsUpstreamError
	if stderrors.As(err, &upstreamError) {
		switch upstreamError.StatusCode {
		case http.StatusBadRequest:
			rec.ErrCode = "INVALID_STATEMENTS_PARAMETERS"
			writeResourceError(c, http.StatusBadRequest, "InvalidArgument", "invalid BYOC Statements request", nil)
		case http.StatusNotFound:
			rec.ErrCode = "STATEMENTS_NOT_FOUND"
			message := "BYOC Statements data is not available for this cluster"
			if operation == service.BYOCStatementsOperationPlanDetail {
				message = "statement plan detail not found"
			}
			writeResourceError(c, http.StatusNotFound, "NotFound", message, nil)
		case http.StatusTooManyRequests:
			rec.ErrCode = "STATEMENTS_RATE_LIMITED"
			writeResourceError(c, http.StatusTooManyRequests, "ResourceExhausted", "BYOC Statements rate limit exceeded", nil)
		case http.StatusRequestTimeout, http.StatusGatewayTimeout:
			rec.ErrCode = "STATEMENTS_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "BYOC Statements request timed out", nil)
		case http.StatusServiceUnavailable:
			rec.ErrCode = "STATEMENTS_UNAVAILABLE"
			writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "BYOC Statements service is temporarily unavailable", nil)
		default:
			rec.ErrCode = "STATEMENTS_UPSTREAM_ERROR"
			writeResourceError(c, http.StatusBadGateway, "BadGateway", "BYOC Statements upstream request failed", nil)
		}
		return
	}

	rec.ErrCode = "STATEMENTS_INTERNAL_ERROR"
	log.Error(c.Request.Context(), "BYOC Statements request failed",
		gozap.String("cluster_id", clusterID), gozap.String("error_type", fmt.Sprintf("%T", err)))
	writeResourceError(c, http.StatusBadGateway, "BadGateway", "BYOC Statements request failed", nil)
}

// Keep the canonical OpenAPI error type reachable by the annotation parser.
var _ apierrors.APIError
