package cluster

import (
	"context"
	"fmt"
	"net/http"
	"regexp"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/api/common"
	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	cliniclog "github.com/pingcap-inc/clinic/pkg/log"
	"github.com/pingcap-inc/clinic/pkg/nextgen_client"
)

const (
	defaultPlanReplayerPageSize = 100
	maxPlanReplayerPageSize     = 1000
	maxPlanReplayerPageTokenLen = 16384
	maxPlanReplayerFileNameLen  = 1024
	planReplayerInternalEmail   = "clinic@pingcap.com"
)

var planReplayerFileNamePattern = regexp.MustCompile(`^replayer_.+\.zip$`)

type planReplayerAccessSources struct {
	getMgmtCluster func(context.Context, uint64) (*mgmtv1.Cluster, error)
	listFiles      func(context.Context, uint64, int, string) (*nextgen_client.ListPlanReplayerFilesResponse, error)
	generateURL    func(context.Context, uint64, *nextgen_client.GeneratePlanReplayerFileDownloadURLRequest) (*nextgen_client.GeneratePlanReplayerFileDownloadURLResponse, error)
}

func (s *ClusterService) planReplayerSources() planReplayerAccessSources {
	return planReplayerAccessSources{
		getMgmtCluster: func(_ context.Context, clusterID uint64) (*mgmtv1.Cluster, error) {
			return datasource.RealtimeGetMgmtCluster(planReplayerMgmtUser(), clusterID)
		},
		listFiles:   datasource.ListPlanReplayerFiles,
		generateURL: datasource.GeneratePlanReplayerFileDownloadURL,
	}
}

func planReplayerMgmtUser() *session.CtxUser {
	return &session.CtxUser{
		Internal: true,
		Email:    planReplayerInternalEmail,
	}
}

func planReplayerLogger(c *gin.Context) *zap.Logger {
	return cliniclog.WithContextL(c.Request.Context()).With(
		zap.String("trace_id", planReplayerTraceID(c)),
		zap.String("api", "plan_replayer"),
	)
}

func planReplayerTraceID(c *gin.Context) string {
	spanContext := trace.SpanContextFromContext(c.Request.Context())
	if spanContext.HasTraceID() {
		return spanContext.TraceID().String()
	}
	return c.GetHeader("X-Trace-ID")
}

func (s *ClusterService) authorizePlanReplayer(c *gin.Context) (uint64, planReplayerAccessSources, bool) {
	logger := planReplayerLogger(c)
	_, _, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		logger.Warn("plan replayer cluster lookup failed", zap.Error(err))
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return 0, planReplayerAccessSources{}, false
	}
	if !ok {
		logger.Warn("plan replayer access denied")
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return 0, planReplayerAccessSources{}, false
	}

	clusterID, err := strconv.ParseUint(cluster.ClusterID, 10, 64)
	if err != nil || clusterID == 0 {
		logger.Warn("invalid plan replayer cluster id", zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("invalid cluster id"))
		return 0, planReplayerAccessSources{}, false
	}
	logger = logger.With(zap.Uint64("cluster_id", clusterID))

	sources := s.planReplayerSources()
	mgmtCluster, err := sources.getMgmtCluster(c.Request.Context(), clusterID)
	if err != nil {
		logger.Error("get plan replayer status from mgmt failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("get plan replayer status failed"))
		return 0, planReplayerAccessSources{}, false
	}
	if !planReplayerEnabled(mgmtCluster) {
		logger.Info("plan replayer is disabled by mgmt gate")
		_ = c.Error(error2.ErrForbidden.New("plan replayer is not enabled for this cluster"))
		return 0, planReplayerAccessSources{}, false
	}
	logger.Info("plan replayer mgmt gate passed")

	return clusterID, sources, true
}

func planReplayerEnabled(cluster *mgmtv1.Cluster) bool {
	return cluster != nil && cluster.GetPlanReplayerStatus() != nil && cluster.GetPlanReplayerStatus().GetEnabled()
}

// ListPlanReplayerFiles lists manual Plan Replayer archives for a Premium logical cluster.
//
//	@Summary	List Plan Replayer files
//	@Tags		Cluster
//	@Produce	json
//	@Param		x-csrf-token	header		string	true	"get value from login.ValidationResp response"
//	@Param		oid				path		string	true	"organization id"
//	@Param		cid				path		string	true	"logical cluster id"
//	@Param		pageSize		query		int		false	"page size, default 100, max 1000"
//	@Param		pageToken		query		string	false	"opaque page token"
//	@Success	200				{object}	nextgen_client.ListPlanReplayerFilesResponse
//	@Router		/orgs/{oid}/clusters/{cid}/plan-replayer/files [get]
func (s *ClusterService) ListPlanReplayerFiles(c *gin.Context) {
	logger := planReplayerLogger(c)
	pageSize, err := planReplayerPageSize(c.Query("pageSize"))
	if err != nil {
		logger.Warn("invalid plan replayer page size", zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	pageToken := c.Query("pageToken")
	if len(pageToken) > maxPlanReplayerPageTokenLen {
		logger.Warn("plan replayer page token is too long", zap.Int("page_token_length", len(pageToken)))
		_ = c.Error(error2.ErrBadRequest.New("pageToken is too long"))
		return
	}
	logger.Info("list plan replayer files requested", zap.Int("page_size", pageSize), zap.Bool("has_page_token", pageToken != ""))

	clusterID, sources, ok := s.authorizePlanReplayer(c)
	if !ok {
		return
	}
	response, err := sources.listFiles(c.Request.Context(), clusterID, pageSize, pageToken)
	if err != nil {
		logger.With(zap.Uint64("cluster_id", clusterID)).Error("list plan replayer files failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("list plan replayer files failed"))
		return
	}
	logger.With(zap.Uint64("cluster_id", clusterID)).Info("list plan replayer files succeeded",
		zap.Int("file_count", len(response.Files)),
		zap.Bool("has_next_page_token", response.NextPageToken != ""),
	)
	c.JSON(http.StatusOK, response)
}

// GeneratePlanReplayerFileDownloadURL returns a short-lived URL for one listed Plan Replayer file.
//
//	@Summary	Generate a Plan Replayer download URL
//	@Tags		Cluster
//	@Accept		json
//	@Produce	json
//	@Param		x-csrf-token	header		string														true	"get value from login.ValidationResp response"
//	@Param		oid				path		string														true	"organization id"
//	@Param		cid				path		string														true	"logical cluster id"
//	@Param		request			body		nextgen_client.GeneratePlanReplayerFileDownloadURLRequest	true	"file name from list response"
//	@Success	200				{object}	nextgen_client.GeneratePlanReplayerFileDownloadURLResponse
//	@Router		/orgs/{oid}/clusters/{cid}/plan-replayer/files/download-url [post]
func (s *ClusterService) GeneratePlanReplayerFileDownloadURL(c *gin.Context) {
	logger := planReplayerLogger(c)
	request := &nextgen_client.GeneratePlanReplayerFileDownloadURLRequest{}
	if err := c.ShouldBindJSON(request); err != nil {
		logger.Warn("invalid plan replayer download URL request", zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("invalid request body"))
		return
	}
	if !validPlanReplayerFileName(request.FileName) {
		logger.Warn("invalid plan replayer file name", zap.Int("file_name_length", len(request.FileName)))
		_ = c.Error(error2.ErrBadRequest.New("invalid plan replayer fileName"))
		return
	}
	logger.Info("generate plan replayer download URL requested")

	clusterID, sources, ok := s.authorizePlanReplayer(c)
	if !ok {
		return
	}
	response, err := sources.generateURL(c.Request.Context(), clusterID, request)
	if err != nil {
		logger.With(zap.Uint64("cluster_id", clusterID)).Error("generate plan replayer download URL failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("generate plan replayer download URL failed"))
		return
	}
	logger.With(zap.Uint64("cluster_id", clusterID)).Info("generate plan replayer download URL succeeded",
		zap.Bool("has_download_url", response.DownloadURL != ""),
		zap.Bool("has_expire_time", response.ExpireTime != ""),
	)
	c.JSON(http.StatusOK, response)
}

func planReplayerPageSize(value string) (int, error) {
	if value == "" || value == "0" {
		return defaultPlanReplayerPageSize, nil
	}
	pageSize, err := strconv.Atoi(value)
	if err != nil || pageSize < 1 || pageSize > maxPlanReplayerPageSize {
		return 0, fmt.Errorf("pageSize must be between 0 and %d", maxPlanReplayerPageSize)
	}
	return pageSize, nil
}

func validPlanReplayerFileName(fileName string) bool {
	return len(fileName) <= maxPlanReplayerFileNameLen &&
		!strings.Contains(fileName, "..") &&
		!strings.ContainsAny(fileName, `/\\`) &&
		planReplayerFileNamePattern.MatchString(fileName)
}
