package statements

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/tidb/util/plancodec"
	"go.uber.org/zap"

	apierr "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/log"
	"github.com/pingcap-inc/clinic/pkg/ngm/binaryplan"
	"github.com/pingcap-inc/clinic/pkg/ngm/statements/aggregate"
	"github.com/pingcap-inc/clinic/pkg/ngm/statements/api"
	"github.com/pingcap-inc/clinic/pkg/ngm/utils"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	o11yurls "github.com/pingcap-inc/clinic/pkg/o11y/urls"
)

const (
	DefaultTimeout = time.Second * 30
)

type Service struct {
	Query   api.Query
	Env     string
	o11yCli *o11y.Client
}

func isServerlessDeployType(deployType string) bool {
	return strings.EqualFold(deployType, TierShared) || strings.EqualFold(deployType, TierServerless)
}

func usesCanonicalStatementsStorage(deployType string) bool {
	return isServerlessDeployType(deployType) || strings.EqualFold(deployType, TierDedicated)
}

func (s *Service) resolveStorageTarget(ctx context.Context, header ClusterHeader, category string) (*o11yurls.StorageTarget, error) {
	if header.Provider == nil || header.Region == nil {
		return nil, fmt.Errorf("provider and region are required")
	}
	return o11yurls.NewClusterURLsResolver(s.o11yCli).ResolveStorageTarget(ctx, o11yurls.StorageTargetParam{
		Provider:   *header.Provider,
		Region:     *header.Region,
		TenantID:   header.GetTenantID(),
		ClusterID:  header.GetClusterID(),
		DeployType: *header.DeployType,
	}, category)
}

// @Summary		Get available fields of Statements
// @Tags			statements
// @Description	get available fields of Statements
// @Produce		json
// @Param			X-Provider		header		string	true	"provider aws/gcp"
// @Param			X-Region		header		string	true	"region"
// @Param			X-Org-Id		header		string	true	"organization id from dbaas"
// @Param			X-Project-Id	header		string	true	"project id from dbaas"
// @Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
// @Param			X-Deploy-Type	header		string	true	"deploy type: dedicated/shared/serverless/premium/nextgen/nextgen-host/byoc/byoc-host"
// @Success		200				{object}	[]string
// @Router			/statements/available_fields [get]
func (s *Service) AvailableFieldsHandler(c *gin.Context) {
	var header ClusterHeader
	if err := c.BindHeader(&header); err != nil {
		log.Warn(c.Request.Context(), "failed to bind statements available fields header", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	if err := header.Validate(); err != nil {
		log.Warn(c.Request.Context(), "invalid statements available fields header", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), DefaultTimeout)
	defer cancel()

	switch strings.ToLower(*header.DeployType) {
	case o11y.PremiumType, o11y.NgHostType, o11y.ByocType, o11y.ByocHostType:
		err := utils.ProxyO11yByQueryCluster(ctx,
			c,
			s.o11yCli,
			*header.DeployType,
			s.Env,
			*header.Provider,
			*header.Region,
			header.GetTenantID(),
			header.GetProjectID(),
			header.GetClusterID(),
			"/internal/diagnosis/api/v1/statements/available_fields")
		if err != nil {
			log.Error(ctx, "failed to proxy to o11y", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.Wrap(err, "failed to proxy to o11y"))
			return
		}
		return
	}

	fields := make([]string, 0, len(aggregate.StatementAggregatorFactories))
	for k := range aggregate.StatementAggregatorFactories {
		fields = append(fields, k)
	}
	c.JSON(http.StatusOK, fields)
}

// @Summary		Get statement types of Statements
// @Tags			statements
// @Description	get statement types of Statements
// @Produce		json
// @Param			X-Provider		header		string	true	"provider aws/gcp"
// @Param			X-Region		header		string	true	"region"
// @Param			X-Org-Id		header		string	true	"organization id from dbaas"
// @Param			X-Project-Id	header		string	true	"project id from dbaas"
// @Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
// @Param			X-Deploy-Type	header		string	true	"deploy type: dedicated/shared/serverless/premium/nextgen/nextgen-host/byoc/byoc-host"
// @Success		200				{object}	[]string
// @Router			/statements/stmt_types [get]
func (s *Service) StmtTypesHandler(c *gin.Context) {
	var header ClusterHeader
	if err := c.BindHeader(&header); err != nil {
		log.Warn(c.Request.Context(), "failed to bind statements stmt types header", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	if err := header.Validate(); err != nil {
		log.Warn(c.Request.Context(), "invalid statements stmt types header", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), DefaultTimeout)
	defer cancel()

	switch strings.ToLower(*header.DeployType) {
	case o11y.PremiumType, o11y.NgHostType, o11y.ByocType, o11y.ByocHostType:
		err := utils.ProxyO11yByQueryCluster(ctx,
			c,
			s.o11yCli,
			*header.DeployType,
			s.Env,
			*header.Provider,
			*header.Region,
			header.GetTenantID(),
			header.GetProjectID(),
			header.GetClusterID(),
			"/internal/diagnosis/api/v1/statements/stmt_types")
		if err != nil {
			log.Error(ctx, "failed to proxy to o11y", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.Wrap(err, "failed to proxy to o11y"))
			return
		}
		return
	}

	c.JSON(http.StatusOK, StmtTypes)
}

// @Summary		List overview of Statements
// @Tags			statements
// @Description	list overview of Statements
// @Produce		json
// @Param			X-Provider		header		string		true	"provider aws/gcp"
// @Param			X-Region		header		string		true	"region"
// @Param			X-Org-Id		header		string		true	"organization id from dbaas"
// @Param			X-Project-Id	header		string		true	"project id from dbaas"
// @Param			X-Cluster-Id	header		string		true	"cluster id from dbaas"
// @Param			X-Deploy-Type	header		string		true	"deploy type: dedicated/shared/serverless/premium/nextgen/nextgen-host/byoc/byoc-host"
// @Param			begin_time		query		int			true	"begin time, unix timestamp"
// @Param			end_time		query		int			true	"end time, unix timestamp"
// @Param			schemas			query		[]string	false	"filter by schemas"
// @Param			stmt_types		query		[]string	false	"filter by stmt_types"
// @Param			text			query		string		false	"filter by text"
// @Param			fields			query		string		false	"fields to return, separated by comma"
// @Param			data_source		query		string		false	"set to v2 to use the Statements V2 O11Y datasource"
// @Success		200				{object}	[]api.Model
// @Router			/statements/list [get]
func (s *Service) ListHandler(c *gin.Context) {
	if isStatementV2DataSource(c.Query("data_source")) {
		header, ok := bindAndValidateStatementHeader(c)
		if !ok {
			return
		}
		s.proxyStatementToO11y(c, header, statementsListO11yPath)
		return
	}

	start := time.Now()
	ctx := c.Request.Context()
	var header ClusterHeader
	if err := c.BindHeader(&header); err != nil {
		log.Warn(ctx, "failed to bind statements list header", zap.Error(err), zap.Duration("duration", time.Since(start)))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	if err := header.Validate(); err != nil {
		fields := append(statementsListLogFields(header, ListQuery{}), zap.Error(err), zap.Duration("duration", time.Since(start)))
		log.Warn(ctx, "invalid statements list header", fields...)
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	var query ListQuery
	if err := c.BindQuery(&query); err != nil {
		fields := append(statementsListLogFields(header, query), zap.Error(err), zap.Duration("duration", time.Since(start)))
		log.Warn(ctx, "failed to bind statements list query", fields...)
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	if err := query.Validate(); err != nil {
		fields := append(statementsListLogFields(header, query), zap.Error(err), zap.Duration("duration", time.Since(start)))
		log.Warn(ctx, "invalid statements list query", fields...)
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}

	ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
	defer cancel()

	switch strings.ToLower(*header.DeployType) {
	case o11y.PremiumType, o11y.NgHostType, o11y.ByocType, o11y.ByocHostType:
		err := utils.ProxyO11yByQueryCluster(ctx,
			c,
			s.o11yCli,
			*header.DeployType,
			s.Env,
			*header.Provider,
			*header.Region,
			header.GetTenantID(),
			header.GetProjectID(),
			header.GetClusterID(),
			"/internal/diagnosis/api/v1/statements/list")
		if err != nil {
			fields := append(statementsListLogFields(header, query), zap.Error(err), zap.Duration("duration", time.Since(start)))
			log.Error(ctx, "failed to proxy statements list to o11y", fields...)
			_ = c.Error(apierr.ErrInternal.Wrap(err, "failed to proxy to o11y"))
			return
		}
		fields := append(statementsListLogFields(header, query),
			zap.Int("status_code", c.Writer.Status()),
			zap.Duration("duration", time.Since(start)))
		log.Info(ctx, "proxied statements list from o11y completed", fields...)
		return
	}
	log.Info(ctx, "statements list from storage", statementsListLogFields(header, query)...)
	var fields []string
	if query.Fields != "" {
		fields = strings.Split(query.Fields, ",")
	}
	params := &api.ListParam{
		Provider:     *header.Provider,
		Region:       *header.Region,
		OrgID:        *header.TenantID,
		ProjectID:    *header.ProjectID,
		ClusterID:    *header.ClusterID,
		SharedTier:   isServerlessDeployType(*header.DeployType),
		StartUnixSec: int64(*query.BeginTime),
		EndUnixSec:   int64(*query.EndTime),
		Fields:       fields,
		Schemas:      query.Schemas,
		StmtTypes:    query.StmtTypes,
		Text:         query.Text,
	}
	if usesCanonicalStatementsStorage(*header.DeployType) {
		target, err := s.resolveStorageTarget(ctx, header, o11y.StorageCategoryStatements)
		fields := append(statementsListLogFields(header, query), zap.Error(err), zap.Duration("duration", time.Since(start)))
		if err != nil {
			log.Warn(ctx, "failed to resolve statements storage target", fields...)
			_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
			return
		}
		params.StorageBucket = target.BucketName
		params.StoragePrefix = target.ObjectPath
		log.Info(ctx, "resolved statements list storage target",
			zap.String("operation", "resolve_statements_storage_target"),
			zap.String("cluster_id", header.GetClusterID()),
			zap.String("deploy_type", *header.DeployType),
			zap.String("category", o11y.StorageCategoryStatements))
	}

	res, err := s.Query.List(ctx, params)
	if err != nil {
		fields := append(statementsListLogFields(header, query), zap.Error(err), zap.Duration("duration", time.Since(start)))
		log.Warn(ctx, "failed to query statements list", fields...)
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}

	fields2 := statementsListLogFields(header, query)

	log.Info(ctx, "statements list from storage completed", zap.Int("count", len(res)), zap.Any("fields", fields2))
	c.JSON(http.StatusOK, res)
}

func statementsListLogFields(header ClusterHeader, query ListQuery) []zap.Field {
	return []zap.Field{
		zap.String("provider", stringPtrValue(header.Provider)),
		zap.String("region", stringPtrValue(header.Region)),
		zap.Uint64("org_id", uint64PtrValue(header.TenantID)),
		zap.Uint64("project_id", uint64PtrValue(header.ProjectID)),
		zap.String("cluster_id", stringPtrValue(header.ClusterID)),
		zap.String("deploy_type", stringPtrValue(header.DeployType)),
		zap.Int("begin_time", intPtrValue(query.BeginTime)),
		zap.Int("end_time", intPtrValue(query.EndTime)),
		zap.Int("fields_count", countCSVFields(query.Fields)),
		zap.Int("schemas_count", len(query.Schemas)),
		zap.Int("stmt_types_count", len(query.StmtTypes)),
		zap.Bool("has_text", strings.TrimSpace(query.Text) != ""),
	}
}

func stringPtrValue(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}

func uint64PtrValue(v *uint64) uint64 {
	if v == nil {
		return 0
	}
	return *v
}

func intPtrValue(v *int) int {
	if v == nil {
		return 0
	}
	return *v
}

func countCSVFields(fields string) int {
	if strings.TrimSpace(fields) == "" {
		return 0
	}
	count := 0
	for _, field := range strings.Split(fields, ",") {
		if strings.TrimSpace(field) != "" {
			count++
		}
	}
	return count
}

// @Summary		Get certain statement
// @Tags			statements
// @Description	get certain statement
// @Produce		json
// @Param			X-Provider		header		string	true	"provider aws/gcp"
// @Param			X-Region		header		string	true	"region"
// @Param			X-Org-Id		header		string	true	"organization id from dbaas"
// @Param			X-Project-Id	header		string	true	"project id from dbaas"
// @Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
// @Param			X-Deploy-Type	header		string	true	"deploy type: dedicated/shared/serverless/premium/nextgen/nextgen-host/byoc/byoc-host"
// @Param			begin_time		query		int		true	"begin time, unix timestamp"
// @Param			end_time		query		int		true	"end time, unix timestamp"
// @Param			digest			query		string	true	"the digest of the statement"
// @Param			schema_name		query		string	false	"filter by schema name"
// @Param			data_source		query		string	false	"set to v2 to use the Statements V2 O11Y datasource"
// @Success		200				{object}	[]api.Model
// @Router			/statements/plans [get]
func (s *Service) PlansHandler(c *gin.Context) {
	if isStatementV2DataSource(c.Query("data_source")) {
		header, ok := bindAndValidateStatementHeader(c)
		if !ok {
			return
		}
		s.proxyStatementToO11y(c, header, statementsPlansO11yPath)
		return
	}

	var header ClusterHeader
	if err := c.BindHeader(&header); err != nil {
		log.Warn(c.Request.Context(), "failed to bind statements plans header", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	if err := header.Validate(); err != nil {
		log.Warn(c.Request.Context(), "invalid statements plans header", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	var query PlansQuery
	if err := c.BindQuery(&query); err != nil {
		log.Warn(c.Request.Context(), "failed to bind statements plans query", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	if err := query.Validate(); err != nil {
		log.Warn(c.Request.Context(), "invalid statements plans query", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), DefaultTimeout)
	defer cancel()

	switch strings.ToLower(*header.DeployType) {
	case o11y.PremiumType, o11y.NgHostType, o11y.ByocType, o11y.ByocHostType:
		err := utils.ProxyO11yByQueryCluster(ctx,
			c,
			s.o11yCli,
			*header.DeployType,
			s.Env,
			*header.Provider,
			*header.Region,
			header.GetTenantID(),
			header.GetProjectID(),
			header.GetClusterID(),
			"/internal/diagnosis/api/v1/statements/plans")
		if err != nil {
			log.Error(ctx, "failed to proxy to o11y", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.Wrap(err, "failed to proxy to o11y"))
			return
		}
		return
	}

	params := &api.PlansParam{
		Provider:     *header.Provider,
		Region:       *header.Region,
		OrgID:        *header.TenantID,
		ProjectID:    *header.ProjectID,
		ClusterID:    *header.ClusterID,
		SharedTier:   isServerlessDeployType(*header.DeployType),
		StartUnixSec: int64(*query.BeginTime),
		EndUnixSec:   int64(*query.EndTime),
		Digest:       query.Digest,
		SchemaName:   query.SchemaName,
	}
	if usesCanonicalStatementsStorage(*header.DeployType) {
		target, err := s.resolveStorageTarget(ctx, header, o11y.StorageCategoryStatements)
		if err != nil {
			log.Warn(ctx, "failed to resolve statements storage target", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
			return
		}
		params.StorageBucket = target.BucketName
		params.StoragePrefix = target.ObjectPath
		log.Info(ctx, "resolved statements plans storage target",
			zap.String("operation", "resolve_statements_storage_target"),
			zap.String("cluster_id", header.GetClusterID()),
			zap.String("deploy_type", *header.DeployType),
			zap.String("category", o11y.StorageCategoryStatements))
	}

	res, err := s.Query.Plans(ctx, params)
	if err != nil {
		log.Warn(ctx, "failed to query statements plans", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}

	log.Info(ctx, "statements plans from storage completed",
		zap.String("operation", "query_statements_plans"),
		zap.String("cluster_id", header.GetClusterID()),
		zap.String("deploy_type", *header.DeployType),
		zap.Int("count", len(res)))
	c.JSON(http.StatusOK, res)
}

// @Summary		Get details of plans of the certain statement
// @Tags			statements
// @Description	get details of plans of the certain statement
// @Produce		json
// @Param			X-Provider		header		string		true	"provider aws/gcp"
// @Param			X-Region		header		string		true	"region"
// @Param			X-Org-Id		header		string		true	"organization id from dbaas"
// @Param			X-Project-Id	header		string		true	"project id from dbaas"
// @Param			X-Cluster-Id	header		string		true	"cluster id from dbaas"
// @Param			X-Deploy-Type	header		string		true	"deploy type: dedicated/shared/serverless/premium/nextgen/nextgen-host/byoc/byoc-host"
// @Param			begin_time		query		int			true	"begin time, unix timestamp"
// @Param			end_time		query		int			true	"end time, unix timestamp"
// @Param			digest			query		string		true	"the digest of the statement"
// @Param			plans			query		[]string	true	"the plan digests of the statement"
// @Param			schema_name		query		string		false	"filter by schema name"
// @Param			data_source		query		string		false	"set to v2 to use the Statements V2 O11Y datasource"
// @Success		200				{object}	api.Model
// @Router			/statements/plan/detail [get]
func (s *Service) DetailHandler(c *gin.Context) {
	if isStatementV2DataSource(c.Query("data_source")) {
		header, ok := bindAndValidateStatementHeader(c)
		if !ok {
			return
		}
		s.proxyStatementToO11yWithModifier(c, header, statementsPlanDetailO11yPath, addBinaryPlanTextModifier)
		return
	}

	var header ClusterHeader
	if err := c.BindHeader(&header); err != nil {
		log.Warn(c.Request.Context(), "failed to bind statements plan detail header", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	if err := header.Validate(); err != nil {
		log.Warn(c.Request.Context(), "invalid statements plan detail header", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	var query DetailQuery
	if err := c.BindQuery(&query); err != nil {
		log.Warn(c.Request.Context(), "failed to bind statements plan detail query", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}
	if err := query.Validate(); err != nil {
		log.Warn(c.Request.Context(), "invalid statements plan detail query", zap.Error(err))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), DefaultTimeout)
	defer cancel()

	switch strings.ToLower(*header.DeployType) {
	case o11y.PremiumType, o11y.NgHostType, o11y.ByocType, o11y.ByocHostType:
		err := utils.ProxyO11yByQueryClusterWithModifier(ctx,
			c,
			s.o11yCli,
			*header.DeployType,
			s.Env,
			*header.Provider,
			*header.Region,
			header.GetTenantID(),
			header.GetProjectID(),
			header.GetClusterID(),
			"/internal/diagnosis/api/v1/statements/plan/detail",
			addBinaryPlanTextModifier)
		if err != nil {
			log.Error(ctx, "failed to proxy to o11y", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.Wrap(err, "failed to proxy to o11y"))
			return
		}
		return
	}

	params := &api.DetailParam{
		Provider:     *header.Provider,
		Region:       *header.Region,
		OrgID:        *header.TenantID,
		ProjectID:    *header.ProjectID,
		ClusterID:    *header.ClusterID,
		SharedTier:   isServerlessDeployType(*header.DeployType),
		StartUnixSec: int64(*query.BeginTime),
		EndUnixSec:   int64(*query.EndTime),
		Digest:       query.Digest,
		Plans:        query.Plans,
		SchemaName:   query.SchemaName,
	}
	if usesCanonicalStatementsStorage(*header.DeployType) {
		target, err := s.resolveStorageTarget(ctx, header, o11y.StorageCategoryStatements)
		if err != nil {
			log.Warn(ctx, "failed to resolve statements storage target", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
			return
		}
		params.StorageBucket = target.BucketName
		params.StoragePrefix = target.ObjectPath
		log.Info(ctx, "resolved statements plan detail storage target",
			zap.String("operation", "resolve_statements_storage_target"),
			zap.String("cluster_id", header.GetClusterID()),
			zap.String("deploy_type", *header.DeployType),
			zap.String("category", o11y.StorageCategoryStatements))
	}

	res, err := s.Query.Detail(ctx, params)
	if err != nil {
		log.Warn(ctx, "failed to query statements plan detail", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}

	log.Info(ctx, "statements plan detail from storage completed",
		zap.String("operation", "query_statements_plan_detail"),
		zap.String("cluster_id", header.GetClusterID()),
		zap.String("deploy_type", *header.DeployType),
		zap.Bool("found", res != nil))
	c.JSON(http.StatusOK, res)
}

func addBinaryPlanTextModifier(body []byte) (modified []byte, err error) {
	modified = body
	defer func() {
		if recovered := recover(); recovered != nil {
			log.PlainWarn("panic while decoding statement binary plan", zap.Any("recovered", recovered))
			modified = body
			err = nil
		}
	}()

	var resp map[string]interface{}
	if err := json.Unmarshal(body, &resp); err != nil {
		log.PlainWarn("failed to unmarshal statement plan detail body", zap.ByteString("body", body), zap.Error(err))
		return body, nil
	}
	bp, ok := resp["binary_plan"].(string)
	if !ok || bp == "" {
		log.PlainWarn("no binary plan in response", zap.Any("resp", resp))
		return body, nil
	}
	compressedPB, err := binaryplan.BinaryPlanJsonToCompressedPB(bp)
	if err != nil {
		log.PlainWarn("failed to convert binary plan to compressed protobuf", zap.String("binary_plan", bp), zap.Error(err))
		return body, nil
	}
	bpText, err := plancodec.DecodeBinaryPlan(compressedPB)
	if err != nil {
		log.PlainWarn("failed to decode binary plan", zap.String("compressed_pb", compressedPB), zap.Error(err))
		return body, nil
	}
	resp["binary_plan_text"] = bpText
	newBody, err := json.Marshal(resp)
	if err != nil {
		log.PlainWarn("failed to marshal statement plan detail body", zap.Any("resp", resp), zap.Error(err))
		return body, nil
	}
	return newBody, nil
}
