package upload

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/api/common"
	error22 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/config"
	_const "github.com/pingcap-inc/clinic/pkg/const"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/rbac"
	"github.com/pingcap-inc/clinic/pkg/resource_limit"
	"github.com/pingcap-inc/clinic/pkg/upload_file/decrypt"
)

const (
	clusterIdKey      = "cluster_id"
	clusterTypeKey    = "cluster_type"
	clusterRebuild    = "rebuild"
	clusterBegineTime = "begin_time"
	clusterEndTime    = "end_time"
	clusterName       = "cluster_name"
	clusterNameSpace  = "k8s_namespace"
	packageDirSize    = "dir_size"
)

type UploadService struct {
	decrypt      *decrypt.Decrypt
	rbac         *rbac.AuthCasbin
	cfg          *config.Config
	quotalimiter resource_limit.IQuotaLimiter
}

func NewUploadService(decrypt *decrypt.Decrypt, rbac *rbac.AuthCasbin, cfg *config.Config, quotalimiter resource_limit.IQuotaLimiter) *UploadService {
	return &UploadService{
		decrypt:      decrypt,
		rbac:         rbac,
		cfg:          cfg,
		quotalimiter: quotalimiter,
	}
}

// @Summary		Precreate
// @Tags			Upload
// @Description	precreate
// @Accept			multipart/form-data
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			uuid			query		string	true	"a custom hash combining package’s filename, size and modify time"
// @Param			length			query		int		true	"the size of the file"
// @Param			alias			query		string	false	"an optional alias of filename"
// @Param			filename		query		string	true	"actual name of the file"
// @Param			file			formData	file	true	"file"
// @Success		200				{object}	common.PreCreateResponse
// @Router			/diag/precreate [post]
func (us *UploadService) Precreate(c *gin.Context) {
	user := session.GetUser(c)
	if user == nil {
		_ = c.Error(error22.ErrBadRequest.New("invalid user"))
		return
	}

	Precreate(c, user, us.decrypt, us.quotalimiter, us.cfg, us.rbac)
}

func Precreate(c *gin.Context, user *session.CtxUser, decrypt *decrypt.Decrypt, quotalimiter resource_limit.IQuotaLimiter, cfg *config.Config, rbacAuth *rbac.AuthCasbin) {
	fileLen, err := strconv.ParseInt(c.Query("length"), 10, 64)
	if err != nil {
		log.Error("not correct filelen", zap.Error(err))
		_ = c.Error(error22.ErrBadRequest.New("invalid filelen"))
		return
	}

	if !user.Internal && !quotalimiter.AskForResource(resource_limit.UploadTimeQuota, 1, uint64(user.UserID)) {
		_ = c.Error(error22.ErrPreConditionFailed.New("%s", resource_limit.UploadTimeQuota.Reason()))
		return
	}

	meta, err := getClusterIdAndClusterType(c, decrypt)
	if err != nil {
		log.Error("empty meta file", zap.Error(err))
		_ = c.Error(error22.ErrBadRequest.New("invalid meta"))
		return
	}

	if meta == nil || meta.clusterID == "" {
		_ = c.Error(error22.ErrBadRequest.New("invalid cluster"))
		return
	}

	cluster, err := datasource.GetCluster(user, meta.clusterID, user.OrgIDInApiToken)
	if err != nil && err != gorm.ErrRecordNotFound {
		log.Error("preupload failed", zap.Uint("user", user.UserID), zap.Uint("org", user.OrgIDInApiToken), zap.Error(err))
		_ = c.Error(error22.ErrInternal.New("precreate check failed"))
		return
	}

	if cluster != nil && cluster.OrgID != user.OrgIDInApiToken {
		_ = c.Error(error22.ErrForbidden.New("the cluster (id: %s) belong to other org (id: %d).", meta.clusterID, cluster.OrgID))
		return
	}

	ok, err := rbacAuth.Auth(user.UserID, user.Internal, rbac.Cluster(), rbac.WriteActing(), user.OrgIDInApiToken)
	if err != nil || !ok {
		log.Error("preupload failed", zap.Uint("user", user.UserID), zap.Uint("org", user.OrgIDInApiToken), zap.Error(err))
		_ = c.Error(error22.ErrForbidden.New("permission deny"))
		return
	}

	if allow, reason := canUpload(meta.clusterID, uint64(fileLen), user, quotalimiter); !allow {
		_ = c.Error(error22.ErrPreConditionFailed.New("%s", reason))
		return
	}

	externalParams := []common.Pair{
		{
			Key:   _const.ClusterIDParameter,
			Value: meta.clusterID,
		},
		{
			Key:   _const.ClusterTypeParameter,
			Value: meta.clusertType,
		},
		{
			Key:   _const.ClusterRebuildParameter,
			Value: fmt.Sprintf("%v", meta.rebuild),
		},
		{
			Key:   _const.ClusterBegineTimeParameter,
			Value: meta.begineTime,
		},
		{
			Key:   _const.ClusterEndTimeParameter,
			Value: meta.endtime,
		},
		{
			Key:   _const.ClusterNameParameter,
			Value: formatName(meta.clusterName, meta.k8sNamespace),
		},
		{
			Key:   _const.ClusterDirSize,
			Value: strconv.Itoa(meta.dirSize),
		},
	}

	err = common.Proxy(c, cfg.GetUploadEndpoint(), &common.ApiUserContext{
		UserID: user.UserID,
		OrgID:  user.OrgIDInApiToken,
	}, _const.UploadPrecreatePath, externalParams...)
	if err != nil {
		log.Error("proxy request failed", zap.Uint("user", user.UserID), zap.Uint("org", user.OrgIDInApiToken), zap.Error(err))
		_ = c.Error(error22.ErrInternal.New("internal error"))
		return
	}
}

// TODO(shenjun): reduce the query request to db, query the org quota ahead and reuse it
func canUpload(clusterID string, fileLen uint64, u *session.CtxUser, quotalimiter resource_limit.IQuotaLimiter) (bool, string) {
	if u.Internal {
		return true, ""
	}
	orgID := u.OrgIDInApiToken
	// check file length
	if !quotalimiter.AskForResource(resource_limit.UploadSizeQuota, fileLen, orgID) {
		return false, resource_limit.UploadSizeQuota.Reason()
	}
	// check cluster count
	if !quotalimiter.AskForResource(resource_limit.ClusterQuota, 1, orgID, clusterID) {
		return false, resource_limit.ClusterQuota.Reason()
	}
	// check total data size for the cluster, no return error for this checking
	//if !quotalimiter.AskForResource(resource_limit.DataSizeQuota, fileLen, orgID, clusterID) {
	//	return false
	//}
	// check upload cnt for the cluster
	if !quotalimiter.AskForResource(resource_limit.UploadCntPerCluster, 1, orgID, clusterID) {
		return false, resource_limit.UploadCntPerCluster.Reason()
	}
	return true, ""
}

type packageMeta struct {
	clusterID    string
	clusertType  string
	rebuild      bool
	begineTime   string
	endtime      string
	clusterName  string
	k8sNamespace string
	dirSize      int
}

func getClusterIdAndClusterType(context *gin.Context, decrypt *decrypt.Decrypt) (*packageMeta, error) {
	body, err := io.ReadAll(context.Request.Body)
	if err != nil {
		log.Error("read response body failed", zap.Error(err))
		return nil, err
	}
	plaintext, err := decrypt.DecryptString(body)
	if err != nil {
		log.Error("decrypt string failed", zap.Error(err))
		return nil, err
	}
	context.Request.Body = io.NopCloser(bytes.NewReader(body))
	return getPackageMeta(plaintext)
}

func getPackageMeta(plaintext []byte) (*packageMeta, error) {
	meta := make(map[string]any)
	d := json.NewDecoder(bytes.NewBuffer(plaintext))
	d.UseNumber()
	if err := d.Decode(&meta); err != nil {
		log.Error("unmarshal precreate body failed", zap.Error(err))
		return nil, err
	}
	clusterIDStr, ok := meta[clusterIdKey].(string)
	if !ok {
		clusterIDInt, ok := meta[clusterIdKey].(json.Number)
		if !ok {
			return nil, errors.New("invalid cluster id type")
		}

		clusterIDStr = string(clusterIDInt)
	}

	clusterType, ok := meta[clusterTypeKey].(string)
	if !ok {
		return nil, errors.New("invalid cluster type")
	}

	if (clusterIDStr == "" || clusterIDStr == "0") && clusterType == "dm-cluster" {
		ts := uint64(time.Now().Unix())
		clusterIDStr = fmt.Sprintf("%d", (ts<<24)+rand.Uint64())
		log.Info("generate cluster id for dm", zap.String("cluster_id", clusterIDStr))
	}

	return &packageMeta{
		clusterID:    clusterIDStr,
		clusertType:  clusterType,
		rebuild:      getBool(meta[clusterRebuild], true),
		begineTime:   getString(meta[clusterBegineTime]),
		endtime:      getString(meta[clusterEndTime]),
		clusterName:  getString(meta[clusterName]),
		k8sNamespace: getString(meta[clusterNameSpace]),
		dirSize:      getInt(meta[packageDirSize]),
	}, nil
}

func formatName(name, namespace string) string {
	if namespace == "" {
		return name
	}
	return fmt.Sprintf("%s/%s", namespace, name)
}

func getString(v any) string {
	s, ok := v.(string)
	if !ok {
		return ""
	}

	return s
}

func getInt(v any) int {
	s, ok := v.(json.Number)
	if ok {
		n, err := s.Int64()
		if err == nil {
			return int(n)
		}
	}

	return 0
}

func getBool(v any, defaultValue bool) bool {
	b, ok := v.(bool)
	if !ok {
		return defaultValue
	}

	return b
}

// @Summary		Flush
// @Tags			Upload
// @Description	call flush api after all parts of file are uploaded
// @Accept			multipart/form-data
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			uuid			query		string	true	"a custom hash combining package’s filename, size and modify time"
// @Param			issue			query		string	false	"an optional issue Jira issue ID this package should be connected to"
// @Success		200				{object}	common.FlushResponse
// @Router			/diag/flush [post]
func (us *UploadService) Flush(c *gin.Context) {
	user := session.GetUser(c)
	if user == nil {
		_ = c.Error(error22.ErrBadRequest.New("invalid user"))
		return
	}

	Flush(c, user, us.cfg)
}

func Flush(c *gin.Context, user *session.CtxUser, cfg *config.Config) {
	err := common.Proxy(c, cfg.GetUploadEndpoint(), &common.ApiUserContext{
		UserID: user.UserID,
		OrgID:  user.OrgIDInApiToken,
	}, _const.UploadFlushPath)
	if err != nil {
		log.Error("proxy request failed", zap.Uint("user", user.UserID), zap.Uint("org", user.OrgIDInApiToken), zap.Error(err))
		_ = c.Error(error22.ErrInternal.New("internal error"))
		return
	}
}

// @Summary		Upload
// @Tags			Upload
// @Description	upload file part api
// @Accept			application/octet-stream
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			uuid			query		string	true	"a custom hash combining package’s filename, size and modify time"
// @Param			sequence		query		int		true	"the serial number of the file part"
// @Param			length			query		int		true	"the length of the file part"
// @Success		200				{string}	string
// @Router			/diag/upload [post]
func (us *UploadService) Upload(c *gin.Context) {
	user := session.GetUser(c)
	if user == nil {
		_ = c.Error(error22.ErrBadRequest.New("invalid user"))
		return
	}

	Upload(c, user, us.cfg)
}

func Upload(c *gin.Context, user *session.CtxUser, cfg *config.Config) {
	err := common.Proxy(c, cfg.GetUploadEndpoint(), &common.ApiUserContext{
		UserID: user.UserID,
		OrgID:  user.OrgIDInApiToken,
	}, _const.UploadPath)
	if err != nil {
		log.Error("proxy request failed", zap.Uint("user", user.UserID), zap.Uint("org", user.OrgIDInApiToken), zap.Error(err))
		_ = c.Error(error22.ErrInternal.New("internal error"))
		return
	}
}
