package resource_limit

import (
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/db/model"
)

type TenantLimiter struct {
	Name     string
	resource uint64 // default 3
}

func (tl *TenantLimiter) GetQuota(_ uint) uint64 {
	return tl.resource
}

// param: request, userID
func (tl *TenantLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 1 {
		log.Error("correct param is userID")
		return false
	}
	userID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for userID")
		return false
	}
	orgNum, err := model.GetOrganizationCount(userID)
	if err != nil {
		log.Error("get org count failed", zap.Error(err))
		return false
	}
	return uint64(orgNum)+request <= tl.resource
}

type ClusterLimiter struct {
	defaultQuota uint64 // default 10
}

func (cl *ClusterLimiter) GetQuota(orgID uint) uint64 {
	return cl.getQuota(orgID)
}

func (cl *ClusterLimiter) getQuota(orgID uint) uint64 {
	quota, found, err := model.FindOrgQuotaByOrgID(orgID)
	if err != nil {
		// fallback to default quota
		log.Error("find org quota failed", zap.Uint("orgID", orgID), zap.Error(err))
		return cl.defaultQuota
	}
	if !found {
		return cl.defaultQuota
	}
	return quota.ClusterCnt
}

// param: request orgID, clusterID
func (cl *ClusterLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 2 {
		log.Error("correct param is orgID, clusterID")
		return false
	}
	orgID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for orgID")
		return false
	}
	clusterID, ok := option[1].(string)
	if !ok {
		log.Error("no correct type for clusterID")
		return false
	}
	clusterNum, err := model.ClusterCountExceptCurrentClusterByOrgID(orgID, clusterID)
	if err != nil {
		log.Error("get cluster count failed", zap.Error(err))
		return false
	}
	quota := cl.getQuota(orgID)
	return request+uint64(clusterNum) <= quota
}

type DataSizeLimiter struct {
	defaultQuota uint64 // default 53687091200 bytes = 50GB
}

func (dl *DataSizeLimiter) GetQuota(orgID uint) uint64 {
	return dl.getQuota(orgID)
}

func (dl *DataSizeLimiter) getQuota(orgID uint) uint64 {
	quota, found, err := model.FindOrgQuotaByOrgID(orgID)
	if err != nil {
		// fallback to default quota
		log.Error("find org quota failed", zap.Uint("orgID", orgID), zap.Error(err))
		return dl.defaultQuota
	}
	if !found {
		return dl.defaultQuota
	}
	return quota.DataSizeBytesPerCluster
}

// todo@toto: check unit of data size in db
// param: request, clusterID
func (dl *DataSizeLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 2 {
		log.Error("correct param is orgID, clusterID")
		return false
	}
	orgID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for orgID")
		return false
	}
	clusterID, ok := option[1].(string)
	if !ok {
		log.Error("no correct type for clusterID")
		return false
	}
	clusterDataSizeBytes, err := model.FetchTotalClusterDataSize(clusterID)
	if err != nil {
		log.Error("get cluster total data size failed", zap.Error(err))
		return false
	}
	quota := dl.getQuota(orgID)
	return clusterDataSizeBytes+request <= quota
}

type UploadCntLimiter struct {
	resource uint64
}

func (ul *UploadCntLimiter) GetQuota(orgID uint) uint64 {
	return ul.resource
}

// param: request, userID
func (ul *UploadCntLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 1 {
		log.Error("correct param is userID")
		return false
	}
	userID, ok := option[0].(uint64)
	if !ok {
		log.Error("no correct type for userID")
		return false
	}
	uploadTime, err := model.GetTotalUploadCntByUser(userID)
	if err != nil {
		log.Error("get user total upload times failed", zap.Error(err))
		return false
	}
	return uploadTime+request <= ul.resource
}

type UploadCntPerClusterLimiter struct {
	defaultQuota uint64
}

func (limiter *UploadCntPerClusterLimiter) GetQuota(orgID uint) uint64 {
	return limiter.getQuota(orgID)
}

func (limiter *UploadCntPerClusterLimiter) getQuota(orgID uint) uint64 {
	quota, found, err := model.FindOrgQuotaByOrgID(orgID)
	if err != nil {
		// fallback to default quota
		log.Error("find org quota failed", zap.Uint("orgID", orgID), zap.Error(err))
		return limiter.defaultQuota
	}
	if !found {
		return limiter.defaultQuota
	}
	return quota.UploadCntPerClusterPerDay
}

// param: request, orgID, clusterID
func (limiter *UploadCntPerClusterLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 2 {
		log.Error("correct param is orgID, clusterID")
		return false
	}
	orgID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for orgID")
		return false
	}

	clusterID, ok := option[1].(string)
	if !ok {
		log.Error("no correct type for clusterID")
		return false
	}

	cnt, err := model.GetUploadCntPerDayByCluster(clusterID)
	if err != nil {
		log.Error("get upload cnt per day failed", zap.String("clusterID", clusterID), zap.Error(err))
		return false
	}
	quota := limiter.getQuota(orgID)
	return cnt+request <= quota
}

type UploadSizeLimiter struct {
	defaultQuota uint64 // default 3GB
}

func (usl *UploadSizeLimiter) GetQuota(orgID uint) uint64 {
	return usl.getQuota(orgID)
}

func (usl *UploadSizeLimiter) getQuota(orgID uint) uint64 {
	quota, found, err := model.FindOrgQuotaByOrgID(orgID)
	if err != nil {
		// fallback to default quota
		log.Error("find org quota failed", zap.Uint("orgID", orgID), zap.Error(err))
		return usl.defaultQuota
	}
	if !found {
		return usl.defaultQuota
	}
	return quota.FileLenBytes
}

// params: request, orgID
func (usl *UploadSizeLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 1 {
		log.Error("correct param is orgID")
		return false
	}
	orgID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for orgID")
		return false
	}

	quota := usl.getQuota(orgID)
	return request <= quota
}

type ApiTokenLimiter struct {
	resource uint64
}

func (atl *ApiTokenLimiter) GetQuota(orgID uint) uint64 {
	return atl.resource
}

func (atl *ApiTokenLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 2 {
		log.Error("correct param is orgID")
		return false
	}
	orgID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for orgID")
		return false
	}
	userID, ok := option[1].(uint)
	if !ok {
		log.Error("no correct type for userID")
		return false
	}

	_, total, err := model.ListCredentials(orgID, userID, 0, 0)
	if err != nil {
		log.Error("")
	}

	return uint64(total)+request <= atl.resource
}

type HealthReportCntPerClusterLimiter struct {
	defaultQuota uint64
}

func (limiter *HealthReportCntPerClusterLimiter) GetQuota(orgID uint) uint64 {
	return limiter.getQuota(orgID)
}

func (limiter *HealthReportCntPerClusterLimiter) getQuota(orgID uint) uint64 {
	quota, found, err := model.FindOrgQuotaByOrgID(orgID)
	if err != nil {
		// fallback to default quota
		log.Error("find org quota failed", zap.Uint("orgID", orgID), zap.Error(err))
		return limiter.defaultQuota
	}
	if !found {
		return limiter.defaultQuota
	}
	return quota.ReportCntPerClusterPerDay
}

// params: orgID, clusterID, fileUUID
// TODO(shenjun): support health report for cloud
func (limiter *HealthReportCntPerClusterLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 3 {
		log.Error("correct param is orgID, clusterID, fileUUID")
		return false
	}
	orgID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for orgID")
		return false
	}
	clusterID, ok := option[1].(string)
	if !ok {
		log.Error("no correct type for clusterID")
		return false
	}
	fileUUID, ok := option[2].(string)
	if !ok {
		log.Error("no correct type for fileUUID")
		return false
	}
	cnt, err := model.GetReportCntPerDayByClusterExcludeHealthReport(clusterID, fileUUID)
	if err != nil {
		log.Error("get report cnt failed", zap.String("cluster", clusterID), zap.String("fileUUID", fileUUID), zap.Error(err))
		return false
	}
	quota := limiter.getQuota(orgID)
	return cnt+request <= quota
}

type CmpReportCntPerClusterLimiter struct {
	defaultQuota uint64
}

func (limiter *CmpReportCntPerClusterLimiter) GetQuota(orgID uint) uint64 {
	return limiter.getQuota(orgID)
}

func (limiter *CmpReportCntPerClusterLimiter) getQuota(orgID uint) uint64 {
	quota, found, err := model.FindOrgQuotaByOrgID(orgID)
	if err != nil {
		// fallback to default quota
		log.Error("find org quota failed", zap.Uint("orgID", orgID), zap.Error(err))
		return limiter.defaultQuota
	}
	if !found {
		return limiter.defaultQuota
	}
	return quota.ReportCntPerClusterPerDay
}

// params: orgID, clusterID, hash
func (limiter *CmpReportCntPerClusterLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 3 {
		log.Error("correct param is orgID, hash")
		return false
	}
	orgID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for orgID")
		return false
	}
	clusterID, ok := option[1].(string)
	if !ok {
		log.Error("no correct type for clusterID")
		return false
	}
	hash, ok := option[2].(string)
	if !ok {
		log.Error("no correct type for hash")
		return false
	}
	cnt, err := model.GetReportCntPerDayByClusterExcludeCmpReport(clusterID, hash)
	if err != nil {
		log.Error("get report cnt failed", zap.String("cluster", clusterID), zap.String("hash", hash), zap.Error(err))
		return false
	}
	quota := limiter.getQuota(orgID)
	return cnt+request <= quota
}

type ReportCntPerClusterLimiter struct {
	defaultQuota uint64
}

func (limiter *ReportCntPerClusterLimiter) GetQuota(orgID uint) uint64 {
	return limiter.getQuota(orgID)
}

func (limiter *ReportCntPerClusterLimiter) getQuota(orgID uint) uint64 {
	quota, found, err := model.FindOrgQuotaByOrgID(orgID)
	if err != nil {
		// fallback to default quota
		log.Error("find org quota failed", zap.Uint("orgID", orgID), zap.Error(err))
		return limiter.defaultQuota
	}
	if !found {
		return limiter.defaultQuota
	}
	return quota.ReportCntPerClusterPerDay
}

// params: orgID, clusterID, user
func (limiter *ReportCntPerClusterLimiter) TriggerLimit(request uint64, option ...any) bool {
	if len(option) != 3 {
		log.Error("correct param is orgID, hash")
		return false
	}
	orgID, ok := option[0].(uint)
	if !ok {
		log.Error("no correct type for orgID")
		return false
	}
	clusterID, ok := option[1].(string)
	if !ok {
		log.Error("no correct type for clusterID")
		return false
	}
	userID, ok := option[2].(uint)
	if !ok {
		log.Error("no correct type for userID")
		return false
	}
	cnt, err := model.GetReportCntPerDayByClusterCreator(clusterID, userID)
	if err != nil {
		log.Error("get report cnt failed", zap.String("cluster", clusterID), zap.Uint("user", userID), zap.Error(err))
		return false
	}
	quota := limiter.getQuota(orgID)
	return uint64(cnt)+request <= quota
}
