package conprof

import (
	"archive/zip"
	"bytes"
	_ "embed"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"

	gcpstorage "cloud.google.com/go/storage"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/tidbcloud/observability/o11ydiagnosis/pkg/storage"
	"github.com/tidbcloud/observability/o11ydiagnosis/pkg/utils"
)

func InitConProfApi(g *gin.RouterGroup, s *Service) {
	g.GET("/continuous_profiling/group_profiles", s.handleList)
	g.GET("/continuous_profiling/group_profile/detail", s.handleDetail)
	g.GET("/continuous_profiling/action_token", s.handleActionToken)
	g.GET("/continuous_profiling/download", s.handleDownload)
	g.GET("/continuous_profiling/single_profile/view", s.handleSingleView)
}

type Service struct {
	ConProf  ConProf
	cacheDir string
}

func NewConProfService(env string, store storage.Storage, cacheDir string) *Service {
	return &Service{
		ConProf:  NewConProf(env, store),
		cacheDir: cacheDir,
	}
}

type errorResponse struct {
	Error    bool   `json:"error"`
	Message  string `json:"message"`
	Code     string `json:"code"`
	FullText string `json:"full_text"`
}

type clusterHeader struct {
	Provider   string `header:"x-provider" binding:"required"`
	Region     string `header:"x-region" binding:"required"`
	TenantID   string `header:"x-org-id"`
	ProjectID  string `header:"x-project-id"`
	ClusterID  string `header:"x-cluster-id" binding:"required"`
	DeployType string `header:"x-deploy-type"` // dedicated/shared/nextgen/premium
}

type listRequest struct {
	BeginTime int64 `json:"begin_time" form:"begin_time" binding:"required"`
	EndTime   int64 `json:"end_time" form:"end_time" binding:"required"`
}

type listResponseItem struct {
	Ts                  int64          `json:"ts"`
	ProfileDurationSecs int            `json:"profile_duration_secs"`
	State               string         `json:"state"`
	ComponentNum        map[string]int `json:"component_num"`
}

// @Summary		List available continuous profiling groups
// @Tags			conprof
// @Description	List continuous profiling group profiles for a given cluster and time window.
// @Produce		json
// @Param			x-provider		header		string	true	"Cloud provider where profiling data is stored: aws or gcp"
// @Param			x-region		header		string	true	"Cloud region where the profiling bucket is deployed"
// @Param			x-org-id		header		string	false	"Organization ID in TiDB Cloud (if applicable)"
// @Param			x-project-id	header		string	false	"Project ID in TiDB Cloud (if applicable)"
// @Param			x-cluster-id	header		string	true	"Observability cluster ID for the target TiDB deployment"
// @Param			begin_time		query		int		true	"Start time (Unix seconds, inclusive)"
// @Param			end_time		query		int		true	"End time (Unix seconds, inclusive)"
// @Success		200				{object}	[]listResponseItem
// @Router			/continuous_profiling/group_profiles [get]
func (s *Service) handleList(c *gin.Context) {
	f := recordHttpMetric(c, "/group_profiles")
	defer f()
	var header clusterHeader
	if err := c.ShouldBindHeader(&header); err != nil {
		log.Error("handleList: invalid header",
			zap.String("path", c.Request.URL.Path),
			zap.String("method", c.Request.Method),
			zap.String("error", err.Error()),
			zap.Any("headers", c.Request.Header),
		)
		c.JSON(http.StatusBadRequest, errorResponse{
			Error:    true,
			Message:  "invalid header",
			FullText: err.Error(),
		})
		return
	}
	if header.DeployType == "" {
		header.DeployType = "dedicated"
	}
	var req listRequest
	if err := c.ShouldBindQuery(&req); err != nil {
		log.Error("handleList: invalid query params",
			zap.String("path", c.Request.URL.Path),
			zap.String("method", c.Request.Method),
			zap.String("error", err.Error()),
			zap.String("query", c.Request.URL.RawQuery),
		)
		c.JSON(http.StatusBadRequest, errorResponse{
			Error:    true,
			Message:  "invalid params",
			FullText: err.Error(),
		})
		return
	}
	metas, err := s.ConProf.List(c.Request.Context(), &ListParams{
		ClusterInfo: ClusterInfo(header),
		BeginTime:   req.BeginTime,
		EndTime:     req.EndTime,
	})
	if err != nil {
		if e, ok := err.(awserr.Error); ok && e.Code() == s3.ErrCodeNoSuchBucket {
			log.Error("handleList: aws region not deployed",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", header.Region),
				zap.String("provider", header.Provider),
				zap.String("cluster_id", header.ClusterID),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  fmt.Sprintf("aws region '%s' not deployed", header.Region),
				FullText: err.Error(),
			})
		} else if err == gcpstorage.ErrBucketNotExist {
			log.Error("handleList: gcp region not deployed",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", header.Region),
				zap.String("provider", header.Provider),
				zap.String("cluster_id", header.ClusterID),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  fmt.Sprintf("gcp region '%s' not deployed", header.Region),
				FullText: err.Error(),
			})
		} else {
			log.Error("handleList: failed to query conprof list",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", header.Region),
				zap.String("provider", header.Provider),
				zap.String("cluster_id", header.ClusterID),
				zap.Int64("begin_time", req.BeginTime),
				zap.Int64("end_time", req.EndTime),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  "failed to query conprof list",
				FullText: err.Error(),
			})
		}
		return
	}
	resp := convertProfileMetasToListResponseItems(metas)
	c.JSON(http.StatusOK, resp)
}

type detailRequest struct {
	Ts int64 `json:"ts" form:"ts" binding:"required"`
}

type detailResponse struct {
	Ts                  int64                   `json:"ts"`
	ProfileDurationSecs int                     `json:"profile_duration_secs"`
	State               string                  `json:"state"`
	TargetProfiles      []detailResponseProfile `json:"target_profiles"`
}

type detailResponseProfile struct {
	State       string                      `json:"state"`
	Error       string                      `json:"error"`
	ProfileType string                      `json:"profile_type"`
	Target      detailResponseProfileTarget `json:"target"`
}

type detailResponseProfileTarget struct {
	Address   string `json:"address"`
	Component string `json:"component"`
}

// @Summary		Get details for a specific profiling group
// @Tags			conprof
// @Description	Fetch the target profiles inside a continuous profiling group at the given timestamp.
// @Produce		json
// @Param			x-provider		header		string	true	"Cloud provider where profiling data is stored: aws or gcp"
// @Param			x-region		header		string	true	"Cloud region where the profiling bucket is deployed"
// @Param			x-org-id		header		string	false	"Organization ID in TiDB Cloud (if applicable)"
// @Param			x-project-id	header		string	false	"Project ID in TiDB Cloud (if applicable)"
// @Param			x-cluster-id	header		string	true	"Observability cluster ID for the target TiDB deployment"
// @Param			ts				query		int		true	"Unix timestamp in seconds identifying the profiling group"
// @Success		200				{object}	detailResponse
// @Router			/continuous_profiling/group_profile/detail [get]
func (s *Service) handleDetail(c *gin.Context) {
	f := recordHttpMetric(c, "/group_profile/detail")
	defer f()
	var header clusterHeader
	if err := c.ShouldBindHeader(&header); err != nil {
		log.Error("handleDetail: invalid header",
			zap.String("path", c.Request.URL.Path),
			zap.String("method", c.Request.Method),
			zap.String("error", err.Error()),
			zap.Any("headers", c.Request.Header),
		)
		c.JSON(http.StatusBadRequest, errorResponse{
			Error:    true,
			Message:  "invalid header",
			FullText: err.Error(),
		})
		return
	}
	var req detailRequest
	if err := c.ShouldBindQuery(&req); err != nil {
		log.Error("handleDetail: invalid query params",
			zap.String("path", c.Request.URL.Path),
			zap.String("method", c.Request.Method),
			zap.String("error", err.Error()),
			zap.String("query", c.Request.URL.RawQuery),
		)
		c.JSON(http.StatusBadRequest, errorResponse{
			Error:    true,
			Message:  "invalid params",
			FullText: err.Error(),
		})
		return
	}
	metas, err := s.ConProf.List(c.Request.Context(), &ListParams{
		ClusterInfo: ClusterInfo(header),
		BeginTime:   req.Ts,
		EndTime:     req.Ts,
	})
	if err != nil {
		if e, ok := err.(awserr.Error); ok && e.Code() == s3.ErrCodeNoSuchBucket {
			log.Error("handleDetail: aws region not deployed",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", header.Region),
				zap.String("provider", header.Provider),
				zap.String("cluster_id", header.ClusterID),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  fmt.Sprintf("aws region '%s' not deployed", header.Region),
				FullText: err.Error(),
			})
		} else if err == gcpstorage.ErrBucketNotExist {
			log.Error("handleDetail: gcp region not deployed",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", header.Region),
				zap.String("provider", header.Provider),
				zap.String("cluster_id", header.ClusterID),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  fmt.Sprintf("gcp region '%s' not deployed", header.Region),
				FullText: err.Error(),
			})
		} else {
			log.Error("handleDetail: failed to query conprof detail",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", header.Region),
				zap.String("provider", header.Provider),
				zap.String("cluster_id", header.ClusterID),
				zap.Int64("ts", req.Ts),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  "failed to query conprof detail",
				FullText: err.Error(),
			})
		}
		return
	}
	resp := convertProfileMetasToDetailResponse(metas)
	c.JSON(http.StatusOK, resp)
}

// @Summary		Create a token for profile download/view
// @Tags			conprof
// @Description	Generate a signed token used by the download and single-view endpoints to authorize profiling file generation.
// @Produce		json
// @Param			x-provider		header		string	true	"Cloud provider where profiling data is stored: aws or gcp"
// @Param			x-region		header		string	true	"Cloud region where the profiling bucket is deployed"
// @Param			x-org-id		header		string	false	"Organization ID in TiDB Cloud (if applicable)"
// @Param			x-project-id	header		string	false	"Project ID in TiDB Cloud (if applicable)"
// @Param			x-cluster-id	header		string	true	"Observability cluster ID for the target TiDB deployment"
// @Param			q				query		string	true	"Raw query string for the target profile, e.g. ts=...&address=...&component=...&profile_type=..."
// @Success		200				{string}	string
// @Router			/continuous_profiling/action_token [get]
func (s *Service) handleActionToken(c *gin.Context) {
	f := recordHttpMetric(c, "/action_token")
	defer f()
	var header clusterHeader
	if err := c.ShouldBindHeader(&header); err != nil {
		log.Error("handleActionToken: invalid header",
			zap.String("path", c.Request.URL.Path),
			zap.String("method", c.Request.Method),
			zap.String("error", err.Error()),
			zap.Any("headers", c.Request.Header),
		)
		c.JSON(http.StatusBadRequest, errorResponse{
			Error:    true,
			Message:  "invalid header",
			FullText: err.Error(),
		})
		return
	}
	q := c.Query("q")
	params := downloadTokenParams{
		Provider:   header.Provider,
		Region:     header.Region,
		TenantID:   header.TenantID,
		ProjectID:  header.ProjectID,
		ClusterID:  header.ClusterID,
		DeployType: header.DeployType,
		Q:          q,
	}
	b, _ := json.Marshal(params)
	token, err := utils.NewJWTString("conprof", string(b))
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "failed to generate jwt token",
			FullText: err.Error(),
		})
		return
	}
	c.String(http.StatusOK, token)
}

type downloadTokenParams struct {
	Provider   string `json:"provider"`
	Region     string `json:"region"`
	TenantID   string `json:"tenant"`
	ProjectID  string `json:"project"`
	ClusterID  string `json:"cluster"`
	DeployType string `json:"deploy_type"`
	Q          string `json:"q"`
}

type downloadTokenQ struct {
	Ts          int64  `json:"ts" form:"ts" binding:"required"`                     // in secs
	Address     string `json:"address" form:"address" binding:"required"`           // e.g. 127.0.0.1:10080
	Component   string `json:"component" form:"component" binding:"required"`       // tidb/tikv/pd
	ProfileType string `json:"profile_type" form:"profile_type" binding:"required"` // cpu/heap/mutex/goroutine
	DataFormat  string `json:"data_format" form:"data_format"`                      // text/proto
}

// @Summary		Download the selected profile as a ZIP
// @Tags			conprof
// @Description	Download the requested profiling data as a ZIP file (including a README) for offline analysis.
// @Param			token	query	string	true	"Signed token generated by /continuous_profiling/action_token"
// @Success		200		{file}	profile
// @Router			/continuous_profiling/download [get]
func (s *Service) handleDownload(c *gin.Context) {
	f := recordHttpMetric(c, "/download")
	defer f()
	token := c.Query("token")
	b, err := utils.ParseJWTString("conprof", token)
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "invalid jwt token",
			FullText: err.Error(),
		})
		return
	}
	var params downloadTokenParams
	if err := json.Unmarshal([]byte(b), &params); err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "invalid payload from jwt token",
			FullText: err.Error(),
		})
		return
	}
	c.Request.URL.RawQuery = params.Q
	var q downloadTokenQ
	if err := c.ShouldBindQuery(&q); err != nil {
		log.Error("handleDownload: invalid query params",
			zap.String("path", c.Request.URL.Path),
			zap.String("method", c.Request.Method),
			zap.String("error", err.Error()),
			zap.String("query", params.Q),
		)
		c.JSON(http.StatusBadRequest, errorResponse{
			Error:    true,
			Message:  "invalid query params",
			FullText: err.Error(),
		})
		return
	}
	file, err := s.ConProf.Download(c.Request.Context(), &DownloadParams{
		ClusterInfo: ClusterInfo{
			Provider:   params.Provider,
			Region:     params.Region,
			TenantID:   params.TenantID,
			ProjectID:  params.ProjectID,
			ClusterID:  params.ClusterID,
			DeployType: params.DeployType,
		},
		ProfileMeta: profileMetaFromTokenQ(q),
	})
	if err != nil {
		if e, ok := err.(awserr.Error); ok && e.Code() == s3.ErrCodeNoSuchBucket {
			log.Error("handleDownload: aws region not deployed",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", params.Region),
				zap.String("provider", params.Provider),
				zap.String("cluster_id", params.ClusterID),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  fmt.Sprintf("aws region '%s' not deployed", params.Region),
				FullText: err.Error(),
			})
		} else if err == gcpstorage.ErrBucketNotExist {
			log.Error("handleDownload: gcp region not deployed",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", params.Region),
				zap.String("provider", params.Provider),
				zap.String("cluster_id", params.ClusterID),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  fmt.Sprintf("gcp region '%s' not deployed", params.Region),
				FullText: err.Error(),
			})
		} else {
			log.Error("handleDownload: failed to download profile",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", params.Region),
				zap.String("provider", params.Provider),
				zap.String("cluster_id", params.ClusterID),
				zap.String("component", q.Component),
				zap.String("profile_type", q.ProfileType),
				zap.Int64("ts", q.Ts),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  "failed to download profile",
				FullText: err.Error(),
			})
		}
		return
	}
	filename := fmt.Sprintf("profile-%s.zip", time.Unix(q.Ts, 0).Format("2006-01-02_15-04-05"))
	c.Writer.Header().Set("Content-Disposition", "attachment; filename="+filename)
	c.Writer.WriteHeader(http.StatusOK)
	zw := zip.NewWriter(c.Writer)
	address := replaceNonAlphanumericWithDash(q.Address)
	filename = fmt.Sprintf("%v_%v_%v_%v.proto", q.ProfileType, q.Component, address, q.Ts)
	fw, err := zw.CreateHeader(&zip.FileHeader{
		Name:     filename,
		Method:   zip.Deflate,
		Modified: time.Now(),
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "failed to create zip header",
			FullText: err.Error(),
		})
		return
	}
	_, err = fw.Write(file)
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "failed to write prof",
			FullText: err.Error(),
		})
		return
	}
	fw, err = zw.CreateHeader(&zip.FileHeader{
		Name:     "README.md",
		Method:   zip.Deflate,
		Modified: time.Now(),
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "failed to create zip header for READNE.md",
			FullText: err.Error(),
		})
		return
	}
	_, err = fw.Write([]byte(downloadReadme))
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "failed to write README.md",
			FullText: err.Error(),
		})
		return
	}
	err = zw.Close()
	if err != nil {
		log.Error("handle download request failed", zap.Error(err))
		return
	}
}

//go:embed jeprof.in
var jeprof string

// @Summary		View the selected profile in speedscope
// @Tags			conprof
// @Description	Generate a single-profile view artifact that can be consumed by speedscope.
// @Param			token	query	string	true	"Signed token generated by /continuous_profiling/action_token"
// @Success		200		{file}	profile
// @Router			/continuous_profiling/single_profile/view [get]
func (s *Service) handleSingleView(c *gin.Context) {
	f := recordHttpMetric(c, "/single_profile/view")
	defer f()
	token := c.Query("token")
	b, err := utils.ParseJWTString("conprof", token)
	if err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "invalid jwt token",
			FullText: err.Error(),
		})
		return
	}
	var params downloadTokenParams
	if err := json.Unmarshal([]byte(b), &params); err != nil {
		c.JSON(http.StatusInternalServerError, errorResponse{
			Error:    true,
			Message:  "invalid payload from jwt token",
			FullText: err.Error(),
		})
		return
	}
	c.Request.URL.RawQuery = params.Q
	var q downloadTokenQ
	if err := c.ShouldBindQuery(&q); err != nil {
		log.Error("handleSingleView: invalid query params",
			zap.String("path", c.Request.URL.Path),
			zap.String("method", c.Request.Method),
			zap.String("error", err.Error()),
			zap.String("query", params.Q),
		)
		c.JSON(http.StatusBadRequest, errorResponse{
			Error:    true,
			Message:  "invalid query params",
			FullText: err.Error(),
		})
		return
	}
	file, err := s.ConProf.Download(c.Request.Context(), &DownloadParams{
		ClusterInfo: ClusterInfo{
			Provider:   params.Provider,
			Region:     params.Region,
			TenantID:   params.TenantID,
			ProjectID:  params.ProjectID,
			ClusterID:  params.ClusterID,
			DeployType: params.DeployType,
		},
		ProfileMeta: profileMetaFromTokenQ(q),
	})
	if err != nil {
		if e, ok := err.(awserr.Error); ok && e.Code() == s3.ErrCodeNoSuchBucket {
			log.Error("handleSingleView: aws region not deployed",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", params.Region),
				zap.String("provider", params.Provider),
				zap.String("cluster_id", params.ClusterID),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  fmt.Sprintf("aws region '%s' not deployed", params.Region),
				FullText: err.Error(),
			})
		} else if err == gcpstorage.ErrBucketNotExist {
			log.Error("handleSingleView: gcp region not deployed",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", params.Region),
				zap.String("provider", params.Provider),
				zap.String("cluster_id", params.ClusterID),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  fmt.Sprintf("gcp region '%s' not deployed", params.Region),
				FullText: err.Error(),
			})
		} else {
			log.Error("handleSingleView: failed to download profile",
				zap.String("path", c.Request.URL.Path),
				zap.String("region", params.Region),
				zap.String("provider", params.Provider),
				zap.String("cluster_id", params.ClusterID),
				zap.String("component", q.Component),
				zap.String("profile_type", q.ProfileType),
				zap.Int64("ts", q.Ts),
				zap.Error(err),
			)
			c.JSON(http.StatusBadRequest, errorResponse{
				Error:    true,
				Message:  "failed to download profile",
				FullText: err.Error(),
			})
		}
		return
	}
	if shouldConvertJeprofHeap(q, file) {
		textContent, err := convertJeprofHeapProfileToCollapsed(file, s.cacheDir)
		if err != nil {
			c.JSON(http.StatusInternalServerError, errorResponse{
				Error:    true,
				Message:  "failed to convert heap profile",
				FullText: err.Error(),
			})
			return
		}
		file = textContent
		c.Writer.Header().Set("Content-Type", "text/plain")
	}
	c.Writer.WriteHeader(http.StatusOK)
	_, _ = c.Writer.Write(file)
}

func profileMetaFromTokenQ(q downloadTokenQ) ProfileMeta {
	return ProfileMeta{
		Ts:          q.Ts,
		Address:     q.Address,
		Component:   q.Component,
		ProfileType: q.ProfileType,
	}
}

func shouldConvertJeprofHeap(q downloadTokenQ, profile []byte) bool {
	if !strings.EqualFold(q.ProfileType, "heap") {
		return false
	}
	if q.DataFormat != "" && !strings.EqualFold(q.DataFormat, "text") {
		return false
	}
	switch strings.ToLower(q.Component) {
	case "tikv", "tiflash":
		return isJeprofHeapProfile(profile)
	default:
		return false
	}
}

func isJeprofHeapProfile(profile []byte) bool {
	profile = bytes.TrimSpace(profile)
	return bytes.HasPrefix(profile, []byte("--- symbol")) ||
		bytes.HasPrefix(profile, []byte("--- heap")) ||
		bytes.HasPrefix(profile, []byte("heap profile:")) ||
		bytes.HasPrefix(profile, []byte("heap_v2/"))
}

func isJeprofSymbolizedProfile(profile []byte) bool {
	profile = bytes.TrimSpace(profile)
	return bytes.HasPrefix(profile, []byte("--- symbol"))
}

func convertJeprofHeapProfileToCollapsed(profile []byte, cacheDir string) ([]byte, error) {
	profileFile, err := os.CreateTemp(cacheDir, "jeprof-heap-*.prof")
	if err != nil {
		return nil, fmt.Errorf("failed to create heap profile temp file: %w", err)
	}
	profilePath := profileFile.Name()
	defer func() {
		if err := os.Remove(profilePath); err != nil {
			log.Warn("failed to remove heap profile", zap.String("filepath", profilePath), zap.Error(err))
		}
	}()
	if _, err := profileFile.Write(profile); err != nil {
		_ = profileFile.Close()
		return nil, fmt.Errorf("failed to write heap profile: %w", err)
	}
	if err := profileFile.Close(); err != nil {
		return nil, fmt.Errorf("failed to close heap profile: %w", err)
	}

	args := []string{"/dev/stdin", "--collapsed"}
	if isJeprofSymbolizedProfile(profile) {
		args = append(args, profilePath)
	} else {
		binaryFile, err := os.CreateTemp(cacheDir, "jeprof-binary-*.bin")
		if err != nil {
			return nil, fmt.Errorf("failed to create heap profile binary placeholder: %w", err)
		}
		binaryPath := binaryFile.Name()
		if err := binaryFile.Close(); err != nil {
			_ = os.Remove(binaryPath)
			return nil, fmt.Errorf("failed to close heap profile binary placeholder: %w", err)
		}
		defer func() {
			if err := os.Remove(binaryPath); err != nil {
				log.Warn("failed to remove heap profile binary placeholder", zap.String("filepath", binaryPath), zap.Error(err))
			}
		}()
		args = append(args, binaryPath, profilePath)
	}

	cmd := exec.Command("perl", args...) //nolint:gosec
	cmd.Stdin = strings.NewReader(jeprof)
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	textContent, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("%w: %s", err, strings.TrimSpace(stderr.String()))
	}
	return textContent, nil
}

func convertProfileMetasToListResponseItems(metas []ProfileMeta) []listResponseItem {
	resp := []listResponseItem{}
	if len(metas) == 0 {
		return resp
	}
	componentMap := map[int64]map[string]map[string]int{} // ts => component => address => count
	for _, meta := range metas {
		if _, ok := componentMap[meta.Ts]; !ok {
			componentMap[meta.Ts] = map[string]map[string]int{
				"pd":      {},
				"tidb":    {},
				"tikv":    {},
				"tiflash": {},
				"ticdc":   {},
			}
		}
		if _, ok := componentMap[meta.Ts][meta.Component]; !ok {
			// protect for unexpected component name
			componentMap[meta.Ts][meta.Component] = map[string]int{}
		}
		componentMap[meta.Ts][meta.Component][meta.Address]++
		if len(resp) == 0 || resp[len(resp)-1].Ts != meta.Ts {
			resp = append(resp, listResponseItem{
				Ts:                  meta.Ts,
				ProfileDurationSecs: 10,
				State:               "finished",
			})
		}
	}
	for n := range resp {
		resp[n].ComponentNum = map[string]int{}
		for component, m := range componentMap[resp[n].Ts] {
			resp[n].ComponentNum[component] = len(m)
		}
	}
	return resp
}

func convertProfileMetasToDetailResponse(metas []ProfileMeta) *detailResponse {
	if len(metas) == 0 {
		return nil
	}
	resp := &detailResponse{
		Ts:                  metas[0].Ts,
		ProfileDurationSecs: 10,
		State:               "finished",
	}
	for _, meta := range metas {
		resp.TargetProfiles = append(resp.TargetProfiles, detailResponseProfile{
			State:       "finished",
			ProfileType: meta.ProfileType,
			Target: detailResponseProfileTarget{
				Address:   meta.Address,
				Component: meta.Component,
			},
		})
	}
	return resp
}

var regexpInvalidFilenameChar = regexp.MustCompile(`[^a-zA-Z0-9]+`)

func replaceNonAlphanumericWithDash(s string) string {
	return regexpInvalidFilenameChar.ReplaceAllString(s, "-")
}

const downloadReadme = `
To review the profile data whose file name suffix is '.proto' interactively:

$ go tool pprof --http=127.0.0.1:6060 profile_xxx.proto
`
