package marketing

import (
	"encoding/json"
	"errors"
	"fmt"
	"math/rand"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/aliyunsubscriptionmngt"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type ReferralModel struct{}

// ========== Campaign Management (Admin) ==========

func (*ReferralModel) CreateCampaign(req *forms.CreateCampaignReq, operatorEmail string) (*forms.CampaignResponse, error) {
	log.Info("referral: CreateCampaign called",
		zap.String("name", req.Name),
		zap.String("totalBudget", req.TotalBudget.String()),
		zap.String("operator", operatorEmail))

	if req.TotalBudget.LessThanOrEqual(decimal.Zero) {
		return nil, billing_errors.NewInvalidInputError(errors.New("total_budget must be positive"))
	}
	campaign := &entity.ReferralCampaign{
		Name:              req.Name,
		Status:            entity.ReferralCampaignStatus_Active,
		TotalBudget:       req.TotalBudget,
		UsedBudget:        decimal.Zero,
		CreatedBy:         operatorEmail,
		Note:              req.Note,
		IsWebsiteCampaign: req.IsWebsiteCampaign,
	}
	err := campaign.Create(db.GetDB())
	if err != nil {
		log.Error("referral: failed to create campaign", zap.String("name", req.Name), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}

	log.Info("referral: campaign created",
		zap.Uint("campaignID", campaign.ID),
		zap.String("name", campaign.Name),
		zap.String("totalBudget", campaign.TotalBudget.String()))
	return campaignToResponse(campaign), nil
}

func (*ReferralModel) ListCampaigns() (*forms.CampaignListResponse, error) {
	log.Info("referral: ListCampaigns called")

	var campaigns []entity.ReferralCampaign
	err := db.GetDB().Order("id DESC").Find(&campaigns).Error
	if err != nil {
		log.Error("referral: failed to list campaigns", zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}
	resp := &forms.CampaignListResponse{
		Campaigns: make([]forms.CampaignResponse, len(campaigns)),
	}
	for i, c := range campaigns {
		resp.Campaigns[i] = *campaignToResponse(&c)
		resp.TotalBudgetAll = resp.TotalBudgetAll.Add(c.TotalBudget)
		resp.UsedBudgetAll = resp.UsedBudgetAll.Add(c.UsedBudget)
	}
	resp.RemainingAll = resp.TotalBudgetAll.Sub(resp.UsedBudgetAll)

	log.Info("referral: ListCampaigns done", zap.Int("count", len(campaigns)))
	return resp, nil
}

func (*ReferralModel) GetCampaign(id uint) (*forms.CampaignResponse, error) {
	log.Info("referral: GetCampaign called", zap.Uint("campaignID", id))

	campaign := &entity.ReferralCampaign{}
	err := db.GetDB().First(campaign, id).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			log.Warn("referral: campaign not found", zap.Uint("campaignID", id))
			return nil, billing_errors.NewInvalidInputError(fmt.Errorf("campaign %d not found", id))
		}
		log.Error("referral: failed to get campaign", zap.Uint("campaignID", id), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}
	return campaignToResponse(campaign), nil
}

func (*ReferralModel) PauseCampaign(id uint) error {
	log.Info("referral: PauseCampaign called", zap.Uint("campaignID", id))

	result := db.GetDB().Model(&entity.ReferralCampaign{}).Where("id = ? AND status = ?", id, entity.ReferralCampaignStatus_Active).Update("status", entity.ReferralCampaignStatus_Paused)
	if result.Error != nil {
		log.Error("referral: failed to pause campaign", zap.Uint("campaignID", id), zap.Error(result.Error))
		return billing_errors.NewSystemError(result.Error)
	}
	if result.RowsAffected == 0 {
		log.Warn("referral: pause campaign no rows affected", zap.Uint("campaignID", id))
		return billing_errors.NewInvalidInputError(fmt.Errorf("campaign %d not found or not active", id))
	}

	log.Info("referral: campaign paused", zap.Uint("campaignID", id))
	return nil
}

func (*ReferralModel) ResumeCampaign(id uint) error {
	log.Info("referral: ResumeCampaign called", zap.Uint("campaignID", id))

	result := db.GetDB().Model(&entity.ReferralCampaign{}).Where("id = ? AND status = ?", id, entity.ReferralCampaignStatus_Paused).Update("status", entity.ReferralCampaignStatus_Active)
	if result.Error != nil {
		log.Error("referral: failed to resume campaign", zap.Uint("campaignID", id), zap.Error(result.Error))
		return billing_errors.NewSystemError(result.Error)
	}
	if result.RowsAffected == 0 {
		log.Warn("referral: resume campaign no rows affected", zap.Uint("campaignID", id))
		return billing_errors.NewInvalidInputError(fmt.Errorf("campaign %d not found or not paused", id))
	}

	log.Info("referral: campaign resumed", zap.Uint("campaignID", id))
	return nil
}

func (*ReferralModel) EndCampaign(id uint) error {
	log.Info("referral: EndCampaign called", zap.Uint("campaignID", id))

	result := db.GetDB().Model(&entity.ReferralCampaign{}).Where("id = ? AND status IN ?", id, []string{entity.ReferralCampaignStatus_Active, entity.ReferralCampaignStatus_Paused}).Update("status", entity.ReferralCampaignStatus_Ended)
	if result.Error != nil {
		log.Error("referral: failed to end campaign", zap.Uint("campaignID", id), zap.Error(result.Error))
		return billing_errors.NewSystemError(result.Error)
	}
	if result.RowsAffected == 0 {
		log.Warn("referral: end campaign no rows affected", zap.Uint("campaignID", id))
		return billing_errors.NewInvalidInputError(fmt.Errorf("campaign %d not found or already ended", id))
	}

	log.Info("referral: campaign ended", zap.Uint("campaignID", id))
	return nil
}

// ========== Referral Code (User API) ==========

func (m *ReferralModel) GetOrCreateReferralCode(tenantID uint, isWebsiteCampaign bool) (*forms.ReferralCodeResponse, error) {
	log.Info("referral: GetOrCreateReferralCode called",
		zap.Uint("tenantID", tenantID), zap.Bool("isWebsiteCampaign", isWebsiteCampaign))

	campaign := &entity.ReferralCampaign{}
	err := entity.NewReferralCampaignQuerySet(db.GetDB()).
		StatusEq(entity.ReferralCampaignStatus_Active).
		IsWebsiteCampaignEq(isWebsiteCampaign).
		OrderAscByID().
		One(campaign)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, billing_errors.ObjectNotExistsError) {
			log.Info("referral: no active campaign for this type",
				zap.Bool("isWebsiteCampaign", isWebsiteCampaign))
			return nil, billing_errors.NewInvalidInputError(fmt.Errorf("no active %s campaign", map[bool]string{true: "website", false: "promotion"}[isWebsiteCampaign]))
		}
		log.Error("referral: failed to query active campaign",
			zap.Bool("isWebsiteCampaign", isWebsiteCampaign), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}

	return m.getOrCreateCodeForCampaign(tenantID, campaign)
}

func (m *ReferralModel) GetReferralStats(tenantID uint, isWebsiteCampaign bool) (*forms.ReferralStatsResponse, error) {
	log.Info("referral: GetReferralStats called",
		zap.Uint("tenantID", tenantID), zap.Bool("isWebsiteCampaign", isWebsiteCampaign))

	var campaigns []entity.ReferralCampaign
	err := entity.NewReferralCampaignQuerySet(db.GetDB()).
		IsWebsiteCampaignEq(isWebsiteCampaign).
		All(&campaigns)
	if err != nil {
		log.Error("referral: failed to query campaigns for stats",
			zap.Bool("isWebsiteCampaign", isWebsiteCampaign), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}

	campaignIDs := make([]uint, 0, len(campaigns))
	campaignMap := make(map[uint]string)
	for _, c := range campaigns {
		campaignIDs = append(campaignIDs, c.ID)
		campaignMap[c.ID] = c.Name
	}

	var codes []entity.ReferralCode
	if len(campaignIDs) > 0 {
		err = entity.NewReferralCodeQuerySet(db.GetDB()).
			TenantIDEq(tenantID).
			CampaignIDIn(campaignIDs...).
			All(&codes)
		if err != nil {
			log.Error("referral: failed to query referral codes for stats",
				zap.Uint("tenantID", tenantID), zap.Error(err))
			return nil, billing_errors.NewSystemError(err)
		}
	}

	if len(codes) == 0 {
		return &forms.ReferralStatsResponse{
			Codes: []forms.ReferralCodeResponse{},
		}, nil
	}

	codeIDs := make([]uint, len(codes))
	for i, c := range codes {
		codeIDs[i] = c.ID
	}

	var events []entity.ReferralEvent
	err = entity.NewReferralEventQuerySet(db.GetDB()).
		ReferralCodeIDIn(codeIDs...).
		OrderDescByCreatedAt().
		All(&events)
	if err != nil {
		log.Error("referral: failed to query referral events for stats",
			zap.Uint("tenantID", tenantID), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}

	var codeResponses []forms.ReferralCodeResponse
	for _, c := range codes {
		codeResponses = append(codeResponses, *referralCodeToResponse(&c, campaignMap[c.CampaignID]))
	}

	items := make([]forms.ReferralEventItem, len(events))
	for i, e := range events {
		items[i] = forms.ReferralEventItem{
			RefereeTenantID: e.RefereeTenantID,
			EventType:       e.EventType,
			Rewarded:        e.Rewarded,
			RewardAmount:    e.RewardAmount,
			SkipReason:      e.SkipReason,
			CreatedAt:       e.CreatedAt,
		}
	}

	totalReferrals := 0
	var totalEarned decimal.Decimal
	for _, c := range codes {
		totalReferrals += c.ReferralCount
		totalEarned = totalEarned.Add(c.TotalEarned)
	}

	log.Info("referral: GetReferralStats done",
		zap.Uint("tenantID", tenantID), zap.Bool("isWebsiteCampaign", isWebsiteCampaign),
		zap.Int("codeCount", len(codes)), zap.Int("eventCount", len(events)))
	return &forms.ReferralStatsResponse{
		TotalReferrals: totalReferrals,
		TotalEarned:    totalEarned,
		Codes:          codeResponses,
		Events:         items,
	}, nil
}

func (m *ReferralModel) getOrCreateCodeForCampaign(tenantID uint, campaign *entity.ReferralCampaign) (*forms.ReferralCodeResponse, error) {
	code := &entity.ReferralCode{}
	err := entity.NewReferralCodeQuerySet(db.GetDB()).
		TenantIDEq(tenantID).
		CampaignIDEq(campaign.ID).
		One(code)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) && !errors.Is(err, billing_errors.ObjectNotExistsError) {
		log.Error("referral: failed to query referral code",
			zap.Uint("tenantID", tenantID), zap.Uint("campaignID", campaign.ID), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}
	if err != nil {
		code = &entity.ReferralCode{
			TenantID:      tenantID,
			CampaignID:    campaign.ID,
			Code:          generateReferralCode(tenantID),
			ReferralCount: 0,
			TotalEarned:   decimal.Zero,
		}
		log.Info("referral: creating new referral code",
			zap.Uint("tenantID", tenantID), zap.Uint("campaignID", campaign.ID), zap.String("code", code.Code))
		err = code.Create(db.GetDB())
		if err != nil {
			if strings.Contains(err.Error(), "Duplicate") {
				log.Warn("referral: duplicate code on create, fetching existing",
					zap.Uint("tenantID", tenantID), zap.Uint("campaignID", campaign.ID))
				err = entity.NewReferralCodeQuerySet(db.GetDB()).
					TenantIDEq(tenantID).CampaignIDEq(campaign.ID).One(code)
				if err != nil {
					log.Error("referral: failed to fetch existing code after duplicate",
						zap.Uint("tenantID", tenantID), zap.Uint("campaignID", campaign.ID), zap.Error(err))
					return nil, billing_errors.NewSystemError(err)
				}
			} else {
				log.Error("referral: failed to create referral code",
					zap.Uint("tenantID", tenantID), zap.Uint("campaignID", campaign.ID), zap.Error(err))
				return nil, billing_errors.NewSystemError(err)
			}
		} else {
			log.Info("referral: referral code created",
				zap.Uint("tenantID", tenantID), zap.Uint("campaignID", campaign.ID), zap.String("code", code.Code))
		}
	}
	return referralCodeToResponse(code, campaign.Name), nil
}

// ========== Reward Trigger (Inner API) ==========

func (m *ReferralModel) TriggerReward(req *forms.ReferralRewardReq) (*forms.ReferralRewardResp, error) {
	log.Info("referral: TriggerReward called",
		zap.String("referralCode", req.ReferralCode),
		zap.Uint("refereeTenantID", req.RefereeTenantID),
		zap.String("eventType", req.EventType))

	// 1. Look up the referral code
	referralCode := &entity.ReferralCode{}
	err := db.GetDB().Where("code = ?", strings.ToUpper(req.ReferralCode)).First(referralCode).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			log.Warn("referral: referral code not found",
				zap.String("referralCode", req.ReferralCode))
			return nil, billing_errors.NewInvalidInputError(fmt.Errorf("referral code %s not found", req.ReferralCode))
		}
		log.Error("referral: failed to look up referral code",
			zap.String("referralCode", req.ReferralCode), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}

	// 2. Load global reward config
	globalCfg, err := getGlobalRewardConfig()
	if err != nil {
		log.Error("referral: failed to load global reward config", zap.Error(err))
		return nil, err
	}

	resp := &forms.ReferralRewardResp{}

	// 3. Execute in transaction
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		var existingCount int64
		err := tx.Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ? AND event_type = ?", req.RefereeTenantID, req.EventType).Count(&existingCount).Error
		if err != nil {
			log.Error("referral: failed to check duplicate event in tx",
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.String("eventType", req.EventType), zap.Error(err))
			return err
		}
		if existingCount > 0 {
			log.Info("referral: event already exists, skipping",
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.String("eventType", req.EventType))
			resp.SkipReason = "already_processed"
			return nil
		}

		var selectedCampaign *entity.ReferralCampaign
		if referralCode.CampaignID > 0 {
			var campaign entity.ReferralCampaign
			err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
				Where("id = ? AND status = ?", referralCode.CampaignID, entity.ReferralCampaignStatus_Active).
				First(&campaign).Error
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					log.Warn("referral: bound campaign not found or not active",
						zap.Uint("campaignID", referralCode.CampaignID))
				} else {
					log.Error("referral: failed to query bound campaign in tx",
						zap.Uint("campaignID", referralCode.CampaignID), zap.Error(err))
					return err
				}
			} else {
				remaining := campaign.TotalBudget.Sub(campaign.UsedBudget)
				if remaining.GreaterThanOrEqual(req.CreditsAmount) {
					selectedCampaign = &campaign
				}
			}
		}

		event := &entity.ReferralEvent{
			ReferralCodeID:   referralCode.ID,
			ReferrerTenantID: referralCode.TenantID,
			RefereeTenantID:  req.RefereeTenantID,
			EventType:        req.EventType,
		}

		if selectedCampaign == nil {
			event.Rewarded = false
			event.SkipReason = entity.ReferralSkipReason_BudgetExhausted
			err = event.Create(tx)
			if err != nil {
				log.Error("referral: failed to create budget-exhausted event",
					zap.Uint("refereeTenantID", req.RefereeTenantID),
					zap.String("eventType", req.EventType), zap.Error(err))
				return err
			}
			resp.SkipReason = entity.ReferralSkipReason_BudgetExhausted
			log.Warn("referral: reward skipped, campaign budget exhausted or campaign inactive",
				zap.Uint("referrerTenantID", referralCode.TenantID),
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.Uint("campaignID", referralCode.CampaignID),
				zap.String("eventType", req.EventType))
			return nil
		}

		log.Info("referral: selected campaign for reward",
			zap.Uint("campaignID", selectedCampaign.ID),
			zap.String("campaignName", selectedCampaign.Name),
			zap.String("remaining", selectedCampaign.TotalBudget.Sub(selectedCampaign.UsedBudget).String()))

		// Get active billing config for referrer
		billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(referralCode.TenantID)
		if err != nil {
			log.Error("referral: failed to get active billing config for referrer",
				zap.Uint("referrerTenantID", referralCode.TenantID),
				zap.Error(err))
			return err
		}

		// Create credits package for the referrer (A)
		now := time.Now()
		expiredDate := now.AddDate(0, 0, globalCfg.CreditsValidityDays)
		creditsInput := &forms.CreditsPackage{
			StartDate:             now,
			ExpiredDate:           expiredDate,
			TotalCredits:          req.CreditsAmount,
			Usage:                 globalCfg.Usage,
			UsageExtend:           req.EventType,
			Note:                  fmt.Sprintf("Referral reward: tenant %d %s", req.RefereeTenantID, req.EventType),
			DiscountEnabled:       false,
			BindedBillingConfigID: &billingConfigID,
		}

		creditsModel := &CreditsPackageModel{}
		creditsPackage, err := creditsModel.CreateCreditsPackage(referralCode.TenantID, creditsInput)
		if err != nil {
			log.Error("referral: failed to create credits package",
				zap.Uint("referrerTenantID", referralCode.TenantID),
				zap.String("amount", req.CreditsAmount.String()),
				zap.Error(err))
			return err
		}
		log.Info("referral: credits package created",
			zap.Uint("referrerTenantID", referralCode.TenantID),
			zap.String("creditsCode", creditsPackage.CreditsCode),
			zap.String("amount", req.CreditsAmount.String()))

		// Update campaign budget
		result := tx.Model(selectedCampaign).Update("used_budget", gorm.Expr("used_budget + ?", req.CreditsAmount))
		if result.Error != nil {
			log.Error("referral: failed to update campaign budget",
				zap.Uint("campaignID", selectedCampaign.ID),
				zap.String("amount", req.CreditsAmount.String()),
				zap.Error(result.Error))
			return result.Error
		}
		log.Info("referral: campaign budget updated",
			zap.Uint("campaignID", selectedCampaign.ID),
			zap.String("deducted", req.CreditsAmount.String()))

		// Create event record
		event.Rewarded = true
		event.RewardAmount = req.CreditsAmount
		event.CreditsPackageID = creditsPackage.CreditsPackageID
		event.CampaignID = selectedCampaign.ID
		err = event.Create(tx)
		if err != nil {
			log.Error("referral: failed to create referral event",
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.String("eventType", req.EventType), zap.Error(err))
			return err
		}
		log.Info("referral: referral event created",
			zap.Uint("refereeTenantID", req.RefereeTenantID),
			zap.String("eventType", req.EventType),
			zap.Bool("rewarded", true))

		// Update referral code stats
		result = tx.Model(referralCode).Updates(map[string]interface{}{
			"referral_count": gorm.Expr("referral_count + 1"),
			"total_earned":   gorm.Expr("total_earned + ?", req.CreditsAmount),
		})
		if result.Error != nil {
			log.Error("referral: failed to update referral code stats",
				zap.Uint("referralCodeID", referralCode.ID),
				zap.Error(result.Error))
			return result.Error
		}
		log.Info("referral: referral code stats updated",
			zap.Uint("referralCodeID", referralCode.ID),
			zap.String("code", referralCode.Code))

		resp.Rewarded = true
		resp.RewardAmount = req.CreditsAmount
		resp.CreditsCode = creditsPackage.CreditsCode

		log.Info("referral: reward granted successfully",
			zap.Uint("referrerTenantID", referralCode.TenantID),
			zap.Uint("refereeTenantID", req.RefereeTenantID),
			zap.String("eventType", req.EventType),
			zap.String("creditsCode", creditsPackage.CreditsCode),
			zap.String("amount", req.CreditsAmount.String()),
			zap.Uint("campaignID", selectedCampaign.ID))

		return nil
	})

	if err != nil {
		log.Error("referral: TriggerReward transaction failed",
			zap.String("referralCode", req.ReferralCode),
			zap.Uint("refereeTenantID", req.RefereeTenantID),
			zap.String("eventType", req.EventType),
			zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}
	return resp, nil
}

// TriggerRefereeReward grants a reward to the referee (B) instead of the referrer (A).
// It looks up the referral code, deducts budget from a campaign, and creates a credits package for the referee.
func (m *ReferralModel) TriggerRefereeReward(req *forms.ReferralRewardReq) (*forms.ReferralRewardResp, error) {
	log.Info("referral: TriggerRefereeReward called",
		zap.String("referralCode", req.ReferralCode),
		zap.Uint("refereeTenantID", req.RefereeTenantID),
		zap.String("eventType", req.EventType))

	// 1. Look up the referral code
	referralCode := &entity.ReferralCode{}
	err := db.GetDB().Where("code = ?", strings.ToUpper(req.ReferralCode)).First(referralCode).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			log.Warn("referral: referral code not found for referee reward",
				zap.String("referralCode", req.ReferralCode))
			return nil, billing_errors.NewInvalidInputError(fmt.Errorf("referral code %s not found", req.ReferralCode))
		}
		log.Error("referral: failed to look up referral code for referee reward",
			zap.String("referralCode", req.ReferralCode), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}

	// 2. Load global reward config
	globalCfg, err := getGlobalRewardConfig()
	if err != nil {
		log.Error("referral: failed to load global reward config for referee reward", zap.Error(err))
		return nil, err
	}

	resp := &forms.ReferralRewardResp{}

	// 3. Execute in transaction
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		var existingCount int64
		err := tx.Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ? AND event_type = ?", req.RefereeTenantID, req.EventType).Count(&existingCount).Error
		if err != nil {
			log.Error("referral: failed to check duplicate referee event in tx",
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.String("eventType", req.EventType), zap.Error(err))
			return err
		}
		if existingCount > 0 {
			log.Info("referral: referee event already exists, skipping",
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.String("eventType", req.EventType))
			resp.SkipReason = "already_processed"
			return nil
		}

		var selectedCampaign *entity.ReferralCampaign
		if referralCode.CampaignID > 0 {
			var campaign entity.ReferralCampaign
			err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
				Where("id = ? AND status = ?", referralCode.CampaignID, entity.ReferralCampaignStatus_Active).
				First(&campaign).Error
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					log.Warn("referral: bound campaign not found or not active for referee reward",
						zap.Uint("campaignID", referralCode.CampaignID))
				} else {
					log.Error("referral: failed to query bound campaign for referee reward",
						zap.Uint("campaignID", referralCode.CampaignID), zap.Error(err))
					return err
				}
			} else {
				remaining := campaign.TotalBudget.Sub(campaign.UsedBudget)
				if remaining.GreaterThanOrEqual(req.CreditsAmount) {
					selectedCampaign = &campaign
				}
			}
		}

		event := &entity.ReferralEvent{
			ReferralCodeID:   referralCode.ID,
			ReferrerTenantID: referralCode.TenantID,
			RefereeTenantID:  req.RefereeTenantID,
			EventType:        req.EventType,
		}

		if selectedCampaign == nil {
			event.Rewarded = false
			event.SkipReason = entity.ReferralSkipReason_BudgetExhausted
			err = event.Create(tx)
			if err != nil {
				log.Error("referral: failed to create budget-exhausted referee event",
					zap.Uint("refereeTenantID", req.RefereeTenantID),
					zap.String("eventType", req.EventType), zap.Error(err))
				return err
			}
			resp.SkipReason = entity.ReferralSkipReason_BudgetExhausted
			log.Warn("referral: referee reward skipped, campaign budget exhausted or campaign inactive",
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.Uint("campaignID", referralCode.CampaignID),
				zap.String("eventType", req.EventType))
			return nil
		}

		log.Info("referral: selected campaign for referee reward",
			zap.Uint("campaignID", selectedCampaign.ID),
			zap.String("remaining", selectedCampaign.TotalBudget.Sub(selectedCampaign.UsedBudget).String()))

		// Get active billing config for the REFEREE (B)
		billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(req.RefereeTenantID)
		if err != nil {
			log.Error("referral: failed to get active billing config for referee",
				zap.Uint("refereeTenantID", req.RefereeTenantID), zap.Error(err))
			return err
		}

		// Create credits package for the REFEREE (B), not the referrer
		now := time.Now()
		expiredDate := now.AddDate(0, 0, globalCfg.CreditsValidityDays)
		creditsInput := &forms.CreditsPackage{
			StartDate:             now,
			ExpiredDate:           expiredDate,
			TotalCredits:          req.CreditsAmount,
			Usage:                 globalCfg.Usage,
			UsageExtend:           req.EventType,
			Note:                  fmt.Sprintf("Referee reward: %s via referral code %s", req.EventType, referralCode.Code),
			DiscountEnabled:       false,
			BindedBillingConfigID: &billingConfigID,
		}

		creditsModel := &CreditsPackageModel{}
		creditsPackage, err := creditsModel.CreateCreditsPackage(req.RefereeTenantID, creditsInput)
		if err != nil {
			log.Error("referral: failed to create credits package for referee",
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.String("amount", req.CreditsAmount.String()), zap.Error(err))
			return err
		}
		log.Info("referral: referee credits package created",
			zap.Uint("refereeTenantID", req.RefereeTenantID),
			zap.String("creditsCode", creditsPackage.CreditsCode),
			zap.String("amount", req.CreditsAmount.String()))

		// Update campaign budget
		result := tx.Model(selectedCampaign).Update("used_budget", gorm.Expr("used_budget + ?", req.CreditsAmount))
		if result.Error != nil {
			log.Error("referral: failed to update campaign budget for referee reward",
				zap.Uint("campaignID", selectedCampaign.ID), zap.Error(result.Error))
			return result.Error
		}
		log.Info("referral: campaign budget updated for referee reward",
			zap.Uint("campaignID", selectedCampaign.ID),
			zap.String("deducted", req.CreditsAmount.String()))

		// Create event record
		event.Rewarded = true
		event.RewardAmount = req.CreditsAmount
		event.CreditsPackageID = creditsPackage.CreditsPackageID
		event.CampaignID = selectedCampaign.ID
		err = event.Create(tx)
		if err != nil {
			log.Error("referral: failed to create referee referral event",
				zap.Uint("refereeTenantID", req.RefereeTenantID),
				zap.String("eventType", req.EventType), zap.Error(err))
			return err
		}
		log.Info("referral: referee referral event created",
			zap.Uint("refereeTenantID", req.RefereeTenantID),
			zap.String("eventType", req.EventType))

		resp.Rewarded = true
		resp.RewardAmount = req.CreditsAmount
		resp.CreditsCode = creditsPackage.CreditsCode

		log.Info("referral: referee reward granted successfully",
			zap.Uint("refereeTenantID", req.RefereeTenantID),
			zap.Uint("referrerTenantID", referralCode.TenantID),
			zap.String("eventType", req.EventType),
			zap.String("amount", req.CreditsAmount.String()),
			zap.Uint("campaignID", selectedCampaign.ID))

		return nil
	})

	if err != nil {
		log.Error("referral: TriggerRefereeReward transaction failed",
			zap.String("referralCode", req.ReferralCode),
			zap.Uint("refereeTenantID", req.RefereeTenantID),
			zap.String("eventType", req.EventType),
			zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}
	return resp, nil
}

// ========== Promotion Checker Queries ==========

// FindReferralCodeBySignupEvent looks up the referral code string for a tenant that was referred (has a signup event).
// Returns empty string if not a referred user.
func (*ReferralModel) FindReferralCodeBySignupEvent(refereeTenantID uint) (string, error) {
	log.Info("referral: FindReferralCodeBySignupEvent called",
		zap.Uint("refereeTenantID", refereeTenantID))

	var signupEvent entity.ReferralEvent
	err := entity.NewReferralEventQuerySet(db.GetDB()).
		RefereeTenantIDEq(refereeTenantID).
		EventTypeEq(entity.ReferralEventType_Signup).
		One(&signupEvent)
	if err != nil {
		log.Info("referral: no signup event found for tenant",
			zap.Uint("refereeTenantID", refereeTenantID), zap.Error(err))
		return "", err
	}

	var referralCode entity.ReferralCode
	err = entity.NewReferralCodeQuerySet(db.GetDB()).
		IDEq(signupEvent.ReferralCodeID).
		One(&referralCode)
	if err != nil {
		log.Error("referral: failed to find referral code by ID",
			zap.Uint("referralCodeID", signupEvent.ReferralCodeID), zap.Error(err))
		return "", err
	}

	log.Info("referral: found referral code via signup event",
		zap.Uint("refereeTenantID", refereeTenantID),
		zap.String("code", referralCode.Code))
	return referralCode.Code, nil
}

// FindTenantsMissingEvent returns referee_tenant_ids that have prerequisiteEvent
// but not targetEvent, and whose prerequisite event was created within the last 30 days.
func (*ReferralModel) FindTenantsMissingEvent(prerequisiteEvent, targetEvent string) ([]uint, error) {
	log.Info("referral: FindTenantsMissingEvent called",
		zap.String("prerequisiteEvent", prerequisiteEvent),
		zap.String("targetEvent", targetEvent))

	cutoff := time.Now().AddDate(0, 0, -30)

	var candidateEvents []entity.ReferralEvent
	err := entity.NewReferralEventQuerySet(db.GetDB()).
		EventTypeEq(prerequisiteEvent).
		CreatedAtGt(cutoff).
		All(&candidateEvents)
	if err != nil {
		log.Error("referral: failed to query prerequisite events",
			zap.String("prerequisiteEvent", prerequisiteEvent), zap.Error(err))
		return nil, err
	}
	if len(candidateEvents) == 0 {
		log.Info("referral: no candidates found for event check",
			zap.String("prerequisiteEvent", prerequisiteEvent),
			zap.String("targetEvent", targetEvent))
		return nil, nil
	}

	candidateTenantIDs := make([]uint, 0, len(candidateEvents))
	for _, e := range candidateEvents {
		candidateTenantIDs = append(candidateTenantIDs, e.RefereeTenantID)
	}

	var existingEvents []entity.ReferralEvent
	err = entity.NewReferralEventQuerySet(db.GetDB()).
		EventTypeEq(targetEvent).
		RefereeTenantIDIn(candidateTenantIDs...).
		All(&existingEvents)
	if err != nil {
		log.Error("referral: failed to query existing target events",
			zap.String("targetEvent", targetEvent), zap.Error(err))
		return nil, err
	}

	existingSet := make(map[uint]bool, len(existingEvents))
	for _, e := range existingEvents {
		existingSet[e.RefereeTenantID] = true
	}

	var result []uint
	for _, tid := range candidateTenantIDs {
		if !existingSet[tid] {
			result = append(result, tid)
		}
	}

	log.Info("referral: FindTenantsMissingEvent done",
		zap.String("prerequisiteEvent", prerequisiteEvent),
		zap.String("targetEvent", targetEvent),
		zap.Int("candidates", len(candidateEvents)),
		zap.Int("alreadyHaveTarget", len(existingEvents)),
		zap.Int("missing", len(result)))
	return result, nil
}

// HasEvent checks whether a referral event already exists for the given tenant and event type.
func (*ReferralModel) HasEvent(refereeTenantID uint, eventType string) bool {
	count, err := entity.NewReferralEventQuerySet(db.GetDB()).
		RefereeTenantIDEq(refereeTenantID).
		EventTypeEq(eventType).
		Count()
	if err != nil {
		log.Warn("referral: HasEvent query failed",
			zap.Uint("refereeTenantID", refereeTenantID),
			zap.String("eventType", eventType),
			zap.Error(err))
		return true
	}
	log.Info("referral: HasEvent",
		zap.Uint("refereeTenantID", refereeTenantID),
		zap.String("eventType", eventType),
		zap.Int64("count", count))
	return count > 0
}

// TriggerSelfReward triggers a self-referral reward for a tenant's own usage.
// It pre-checks for idempotency before calling TriggerRefereeReward (credits go to self).
func (m *ReferralModel) TriggerSelfReward(tenantID uint, eventType string, creditsAmount decimal.Decimal) {
	log.Info("referral: TriggerSelfReward called",
		zap.Uint("tenantID", tenantID), zap.String("eventType", eventType),
		zap.String("amount", creditsAmount.String()))

	if m.HasEvent(tenantID, eventType) {
		log.Info("referral: TriggerSelfReward skipped, event already exists",
			zap.Uint("tenantID", tenantID), zap.String("eventType", eventType))
		return
	}

	code, err := m.FindReferralCodeBySignupEvent(tenantID)
	if err != nil {
		log.Warn("referral: tenant has no signup referral event, skipping self-reward",
			zap.Uint("tenantID", tenantID), zap.String("eventType", eventType), zap.Error(err))
		return
	}

	_, err = m.TriggerRefereeReward(&forms.ReferralRewardReq{
		ReferralCode:    code,
		RefereeTenantID: tenantID,
		EventType:       eventType,
		CreditsAmount:   creditsAmount,
	})
	if err != nil {
		log.Error("referral: TriggerSelfReward failed",
			zap.Uint("tenantID", tenantID), zap.String("eventType", eventType), zap.Error(err))
	}
}

// TriggerReferrerContinuousUseReward is called after a tenant gets continuous_use reward.
// It looks up the referrer via the signup event and triggers referee_continuous_use reward for the referrer.
func (m *ReferralModel) TriggerReferrerContinuousUseReward(refereeTenantID uint, creditsAmount decimal.Decimal) {
	log.Info("referral: TriggerReferrerContinuousUseReward called",
		zap.Uint("refereeTenantID", refereeTenantID),
		zap.String("amount", creditsAmount.String()))

	if m.HasEvent(refereeTenantID, entity.ReferralEventType_RefereeContinuousUse) {
		log.Info("referral: TriggerReferrerContinuousUseReward skipped, event already exists",
			zap.Uint("refereeTenantID", refereeTenantID))
		return
	}

	// Find the signup event to get the referral code
	var signupEvent entity.ReferralEvent
	err := entity.NewReferralEventQuerySet(db.GetDB()).
		RefereeTenantIDEq(refereeTenantID).
		EventTypeEq(entity.ReferralEventType_Signup).
		One(&signupEvent)
	if err != nil {
		log.Info("referral: no signup event for tenant, not a referred user",
			zap.Uint("refereeTenantID", refereeTenantID))
		return
	}

	var referralCode entity.ReferralCode
	err = entity.NewReferralCodeQuerySet(db.GetDB()).
		IDEq(signupEvent.ReferralCodeID).
		One(&referralCode)
	if err != nil {
		log.Warn("referral: failed to find referral code for referee_continuous_use",
			zap.Uint("referralCodeID", signupEvent.ReferralCodeID), zap.Error(err))
		return
	}

	log.Info("referral: triggering referee_continuous_use reward",
		zap.Uint("refereeTenantID", refereeTenantID),
		zap.Uint("referrerTenantID", referralCode.TenantID),
		zap.String("code", referralCode.Code))

	_, err = m.TriggerReward(&forms.ReferralRewardReq{
		ReferralCode:    referralCode.Code,
		RefereeTenantID: refereeTenantID,
		EventType:       entity.ReferralEventType_RefereeContinuousUse,
		CreditsAmount:   creditsAmount,
	})
	if err != nil {
		log.Error("referral: TriggerReferrerContinuousUseReward failed",
			zap.Uint("refereeTenantID", refereeTenantID), zap.Error(err))
	}
}

// TriggerCommunityReward grants a community payment reward to a tenant.
// Unlike other rewards, community_payment can happen multiple times (per invoice),
// so this method does NOT use referral_events and does NOT check for duplicates.
// It directly creates a credits package and deducts from campaign budget.
func (m *ReferralModel) TriggerCommunityReward(tenantID uint, creditsAmount decimal.Decimal) error {
	log.Info("referral: TriggerCommunityReward called",
		zap.Uint("tenantID", tenantID),
		zap.String("amount", creditsAmount.String()))

	if creditsAmount.LessThanOrEqual(decimal.Zero) {
		log.Info("referral: TriggerCommunityReward skipped, amount <= 0",
			zap.Uint("tenantID", tenantID))
		return nil
	}

	globalCfg, err := getGlobalRewardConfig()
	if err != nil {
		log.Error("referral: failed to load global reward config for community reward", zap.Error(err))
		return err
	}

	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		// Find an active campaign with sufficient budget
		var campaigns []entity.ReferralCampaign
		err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("status = ?", entity.ReferralCampaignStatus_Active).
			Order("id ASC").
			Find(&campaigns).Error
		if err != nil {
			log.Error("referral: failed to query active campaigns for community reward", zap.Error(err))
			return err
		}

		var selectedCampaign *entity.ReferralCampaign
		for i := range campaigns {
			remaining := campaigns[i].TotalBudget.Sub(campaigns[i].UsedBudget)
			if remaining.GreaterThanOrEqual(creditsAmount) {
				selectedCampaign = &campaigns[i]
				break
			}
		}

		if selectedCampaign == nil {
			log.Warn("referral: community reward skipped, budget exhausted",
				zap.Uint("tenantID", tenantID))
			return nil
		}

		// Get active billing config for the tenant
		billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(tenantID)
		if err != nil {
			log.Error("referral: failed to get active billing config for community reward",
				zap.Uint("tenantID", tenantID), zap.Error(err))
			return err
		}

		// Create credits package
		now := time.Now()
		expiredDate := now.AddDate(0, 0, globalCfg.CreditsValidityDays)
		creditsInput := &forms.CreditsPackage{
			StartDate:             now,
			ExpiredDate:           expiredDate,
			TotalCredits:          creditsAmount,
			Usage:                 globalCfg.Usage,
			UsageExtend:           entity.ReferralEventType_CommunityPayment,
			Note:                  fmt.Sprintf("Community reward: %s credits for tenant %d", creditsAmount.String(), tenantID),
			DiscountEnabled:       false,
			BindedBillingConfigID: &billingConfigID,
		}

		creditsModel := &CreditsPackageModel{}
		creditsPackage, err := creditsModel.CreateCreditsPackage(tenantID, creditsInput)
		if err != nil {
			log.Error("referral: failed to create credits package for community reward",
				zap.Uint("tenantID", tenantID),
				zap.String("amount", creditsAmount.String()), zap.Error(err))
			return err
		}

		// Update campaign budget
		result := tx.Model(selectedCampaign).Update("used_budget", gorm.Expr("used_budget + ?", creditsAmount))
		if result.Error != nil {
			log.Error("referral: failed to update campaign budget for community reward",
				zap.Uint("campaignID", selectedCampaign.ID), zap.Error(result.Error))
			return result.Error
		}

		log.Info("referral: community reward granted",
			zap.Uint("tenantID", tenantID),
			zap.String("amount", creditsAmount.String()),
			zap.String("creditsCode", creditsPackage.CreditsCode),
			zap.Uint("campaignID", selectedCampaign.ID))
		return nil
	})

	if err != nil {
		log.Error("referral: TriggerCommunityReward transaction failed",
			zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	return nil
}

// QueryTenantQPS queries Clinic API for total request count of a tenant's clusters.
func (*ReferralModel) QueryTenantQPS(tenantID uint, start, end time.Time) (float64, error) {
	log.Info("referral: QueryTenantQPS called",
		zap.Uint("tenantID", tenantID),
		zap.Time("start", start), zap.Time("end", end))

	clinic := GetClinicHandler()

	clusterIDs, err := clinic.GetTenantClusterIDs(tenantID)
	if err != nil {
		log.Error("referral: failed to get cluster IDs for tenant",
			zap.Uint("tenantID", tenantID), zap.Error(err))
		return 0, fmt.Errorf("failed to query clusters for tenant %d: %w", tenantID, err)
	}
	if len(clusterIDs) == 0 {
		log.Info("referral: tenant has no active clusters",
			zap.Uint("tenantID", tenantID))
		return 0, nil
	}

	var totalRequests float64
	for _, clusterID := range clusterIDs {
		requests, err := clinic.GetClusterRequests(clusterID, start, end)
		if err != nil {
			log.Warn("referral: failed to query cluster requests",
				zap.Uint("tenantID", tenantID),
				zap.String("clusterID", clusterID), zap.Error(err))
			continue
		}
		totalRequests += requests
	}

	log.Info("referral: QueryTenantQPS done",
		zap.Uint("tenantID", tenantID),
		zap.Int("clusterCount", len(clusterIDs)),
		zap.Float64("totalRequests", totalRequests))
	return totalRequests, nil
}

// CheckConsecutiveDays checks if a tenant had >= dailyThreshold requests per day
// for the last `days` consecutive days.
func (m *ReferralModel) CheckConsecutiveDays(tenantID uint, days, dailyThreshold int) bool {
	log.Info("referral: CheckConsecutiveDays called",
		zap.Uint("tenantID", tenantID),
		zap.Int("days", days), zap.Int("dailyThreshold", dailyThreshold))

	now := time.Now()
	for i := 0; i < days; i++ {
		dayEnd := now.AddDate(0, 0, -i)
		dayStart := dayEnd.AddDate(0, 0, -1)
		total, err := m.QueryTenantQPS(tenantID, dayStart, dayEnd)
		if err != nil {
			log.Warn("referral: CheckConsecutiveDays failed to query daily QPS",
				zap.Uint("tenantID", tenantID), zap.Int("daysAgo", i), zap.Error(err))
			return false
		}
		if total < float64(dailyThreshold) {
			log.Info("referral: CheckConsecutiveDays below threshold",
				zap.Uint("tenantID", tenantID), zap.Int("daysAgo", i),
				zap.Float64("total", total), zap.Int("threshold", dailyThreshold))
			return false
		}
	}

	log.Info("referral: CheckConsecutiveDays passed",
		zap.Uint("tenantID", tenantID),
		zap.Int("days", days), zap.Int("dailyThreshold", dailyThreshold))
	return true
}

// ========== Helpers ==========

func campaignToResponse(c *entity.ReferralCampaign) *forms.CampaignResponse {
	return &forms.CampaignResponse{
		ID:                c.ID,
		Name:              c.Name,
		Status:            c.Status,
		TotalBudget:       c.TotalBudget,
		UsedBudget:        c.UsedBudget,
		RemainingBudget:   c.TotalBudget.Sub(c.UsedBudget),
		CreatedAt:         c.CreatedAt,
		CreatedBy:         c.CreatedBy,
		Note:              c.Note,
		IsWebsiteCampaign: c.IsWebsiteCampaign,
	}
}

func referralCodeToResponse(c *entity.ReferralCode, campaignName string) *forms.ReferralCodeResponse {
	shareBase := config.Get().GetString(config.ReferralShareLinkBase)
	return &forms.ReferralCodeResponse{
		CampaignID:    c.CampaignID,
		CampaignName:  campaignName,
		Code:          c.Code,
		ReferralCount: c.ReferralCount,
		TotalEarned:   c.TotalEarned,
		ShareLink:     fmt.Sprintf("%s?referral_code=%s", shareBase, c.Code),
	}
}

func getGlobalRewardConfig() (*forms.ReferralRewardConfig, error) {
	cfgStr := config.Get().GetString(config.ReferralRewardConfig)
	var cfg forms.ReferralRewardConfig
	if err := json.Unmarshal([]byte(cfgStr), &cfg); err != nil {
		log.Error("referral: failed to parse referral reward config",
			zap.String("configStr", cfgStr), zap.Error(err))
		return nil, billing_errors.NewSystemError(fmt.Errorf("invalid referral reward config: %w", err))
	}
	return &cfg, nil
}

func generateReferralCode(tenantID uint) string {
	const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
	suffix := make([]byte, 6)
	for i := range suffix {
		suffix[i] = chars[rand.Intn(len(chars))]
	}
	return fmt.Sprintf("TIDB-%d-%s", tenantID, string(suffix))
}

func (m *ReferralModel) NotifyRegistration(req *forms.NotifyRegistrationReq) (gin.H, error) {
	log.Info("referral: NotifyRegistration called",
		zap.Uint("tenantID", req.TenantID),
		zap.String("websiteReferrerUrl", req.WebsiteReferrerUrl),
		zap.String("utmCampaign", req.UtmCampaign),
		zap.String("utmContent", req.UtmContent),
		zap.String("utmMedium", req.UtmMedium),
		zap.String("utmSource", req.UtmSource),
		zap.String("utmTerm", req.UtmTerm),
		zap.String("mostRecentReferralUrl", req.MostRecentReferralUrl),
		zap.String("signupSource", req.SignupSource),
		zap.String("providerSource", req.ProviderSource),
		zap.String("thirdPartyAccountProvider", req.ThirdPartyAccountProvider),
		zap.String("thirdPartyAccountCode", req.ThirdPartyAccountCode))

	if req.ThirdPartyAccountProvider == constant.AliCloudAccountProvider {
		subscriptionMngt := &aliyunsubscriptionmngt.AliyunSubscriptionMngt{}
		record, err := subscriptionMngt.GetLastSubscriptionRecodByAliUid(req.ThirdPartyAccountCode)
		if err != nil {
			log.Warn("referral: failed to query alicloud subscription, skipping",
				zap.Uint("tenantID", req.TenantID),
				zap.String("thirdPartyAccountCode", req.ThirdPartyAccountCode),
				zap.Error(err))
			return forms.OK, nil
		}
		if !record.IsThirdParty {
			log.Info("referral: alicloud non-third-party account, skipping",
				zap.Uint("tenantID", req.TenantID),
				zap.String("thirdPartyAccountCode", req.ThirdPartyAccountCode))
			return forms.OK, nil
		}
	}

	referralCode := &entity.ReferralCode{}
	if err := db.GetDB().Where("code = ?", strings.ToUpper(req.UtmSource)).First(referralCode).Error; err != nil {
		log.Info("referral: referral code from utm_source not found, skipping reward",
			zap.Uint("tenantID", req.TenantID),
			zap.String("utmSource", req.UtmSource),
			zap.Error(err))
		return forms.OK, nil
	}

	if req.UtmCampaign != "startup_adoption" {
		log.Info("referral: utm_campaign not matched, skipping reward",
			zap.Uint("tenantID", req.TenantID),
			zap.String("utmCampaign", req.UtmCampaign))
		return forms.OK, nil
	}

	log.Info("referral: conditions matched, processing registration reward",
		zap.Uint("tenantID", req.TenantID),
		zap.String("referralCode", req.UtmSource))

	signupAmount := decimal.NewFromFloat(config.Get().GetFloat64(config.ReferralAmountSignup))
	if _, err := m.TriggerReward(&forms.ReferralRewardReq{
		ReferralCode:    req.UtmSource,
		RefereeTenantID: req.TenantID,
		EventType:       entity.ReferralEventType_Signup,
		CreditsAmount:   signupAmount,
	}); err != nil {
		log.Error("referral: failed to trigger referrer signup reward",
			zap.Uint("tenantID", req.TenantID), zap.Error(err))
		return forms.OK, nil
	}

	refereeSignupAmount := decimal.NewFromFloat(config.Get().GetFloat64(config.ReferralAmountRefereeSignup))
	if _, err := m.TriggerRefereeReward(&forms.ReferralRewardReq{
		ReferralCode:    req.UtmSource,
		RefereeTenantID: req.TenantID,
		EventType:       entity.ReferralEventType_RefereeSignup,
		CreditsAmount:   refereeSignupAmount,
	}); err != nil {
		log.Error("referral: failed to trigger referee signup reward",
			zap.Uint("tenantID", req.TenantID), zap.Error(err))
		return forms.OK, nil
	}

	log.Info("referral: registration rewards triggered",
		zap.Uint("tenantID", req.TenantID))

	return forms.OK, nil
}

func (*ReferralModel) GetTenantPromotionStatus(tenantID uint) (*forms.TenantPromotionResponse, error) {
	log.Info("referral: GetTenantPromotionStatus called", zap.Uint("tenantID", tenantID))

	var events []entity.ReferralEvent
	err := entity.NewReferralEventQuerySet(db.GetDB()).
		RefereeTenantIDEq(tenantID).
		All(&events)
	if err != nil {
		log.Error("referral: failed to query referral events for tenant promotion status",
			zap.Uint("tenantID", tenantID), zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}

	campaignIDSet := make(map[uint]bool)
	for _, e := range events {
		if e.CampaignID > 0 {
			campaignIDSet[e.CampaignID] = true
		}
	}

	isPromotion := false
	if len(campaignIDSet) > 0 {
		campaignIDs := make([]uint, 0, len(campaignIDSet))
		for id := range campaignIDSet {
			campaignIDs = append(campaignIDs, id)
		}
		count, err := entity.NewReferralCampaignQuerySet(db.GetDB()).
			IDIn(campaignIDs...).
			IsWebsiteCampaignEq(true).
			Count()
		if err != nil {
			log.Error("referral: failed to query website campaigns",
				zap.Any("campaignIDs", campaignIDs), zap.Error(err))
			return nil, billing_errors.NewSystemError(err)
		}
		isPromotion = count > 0
	}

	log.Info("referral: GetTenantPromotionStatus done",
		zap.Uint("tenantID", tenantID),
		zap.Bool("isPromotion", isPromotion))
	return &forms.TenantPromotionResponse{
		TenantID:    tenantID,
		IsPromotion: isPromotion,
	}, nil

}
