package inviteactivity

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"sort"
	"strings"
	"time"

	"google.golang.org/grpc"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	domain "hyapp/services/activity-service/internal/domain/inviteactivity"
)

const grantReason = "invite_activity_reward"

type Repository interface {
	GetInviteActivityRewardConfig(ctx context.Context) (domain.Config, bool, error)
	UpdateInviteActivityRewardConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error)
	GetInviteActivityRewardProgress(ctx context.Context, cycleKey string, userID int64) (domain.Progress, bool, error)
	ListInviteActivityRewardClaimsByUserCycle(ctx context.Context, cycleKey string, userID int64) ([]domain.Claim, error)
	ConsumeInviteActivityRechargeEvent(ctx context.Context, event domain.RechargeEvent, cycle domain.Cycle, nowMS int64) (domain.ConsumeResult, error)
	RecordInviteActivityInviteEvent(ctx context.Context, event domain.InviteEvent, cycle domain.Cycle, status string, skipReason string, nowMS int64) (domain.ConsumeResult, error)
	ConsumeInviteActivityValidInviteEvent(ctx context.Context, event domain.ValidInviteEvent, cycle domain.Cycle, nowMS int64) (domain.ConsumeResult, error)
	CreateInviteActivityRewardClaim(ctx context.Context, claim domain.Claim, nowMS int64) (domain.Claim, error)
	MarkInviteActivityRewardClaimGranted(ctx context.Context, claimID string, walletTransactionID string, grantedAtMS int64) (domain.Claim, error)
	MarkInviteActivityRewardClaimFailed(ctx context.Context, claimID string, failureReason string, nowMS int64) error
	ListInviteActivityRewardClaims(ctx context.Context, query domain.ClaimQuery) ([]domain.Claim, int64, error)
	ListInviteActivityRewardSummaries(ctx context.Context, query domain.SummaryQuery) ([]domain.Summary, int64, error)
	ListInviteActivityLeaderboard(ctx context.Context, query domain.LeaderboardQuery) ([]domain.LeaderboardEntry, int64, error)
}

type WalletClient interface {
	CreditInviteActivityReward(ctx context.Context, req *walletv1.CreditInviteActivityRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditInviteActivityRewardResponse, error)
}

type InviteAttributionSource interface {
	GetInviteAttribution(ctx context.Context, invitedUserID int64) (*userv1.InviteAttribution, error)
}

// InviteeRepository 是 App 邀请明细的窄读接口。与后台 summary 分开，避免 H5 查询触发后台聚合和无界子列表。
type InviteeRepository interface {
	ListInviteActivityRewardInvitees(ctx context.Context, query domain.InviteeQuery) ([]domain.InviteeSummary, int64, error)
}

type Service struct {
	repository Repository
	wallet     WalletClient
	invite     InviteAttributionSource
	now        func() time.Time
}

func New(repository Repository, wallet WalletClient, invite InviteAttributionSource) *Service {
	return &Service{repository: repository, wallet: wallet, invite: invite, now: time.Now}
}

func (s *Service) SetClock(now func() time.Time) {
	if now != nil {
		s.now = now
	}
}

func (s *Service) GetStatus(ctx context.Context, userID int64) (domain.StatusResult, error) {
	if err := s.requireRepository(); err != nil {
		return domain.StatusResult{}, err
	}
	if userID <= 0 {
		return domain.StatusResult{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	now := s.now().UTC()
	cycle := CycleForTime(now)
	result := domain.StatusResult{Cycle: cycle, ServerTimeMS: now.UnixMilli()}

	config, exists, err := s.repository.GetInviteActivityRewardConfig(ctx)
	if err != nil {
		return domain.StatusResult{}, err
	}
	if exists {
		config.Tiers = activeTiers(config.Tiers)
		result.Config = config
	} else {
		result.Config = domain.Config{AppCode: appcode.FromContext(ctx), Enabled: true}
	}
	progress, _, err := s.repository.GetInviteActivityRewardProgress(ctx, cycle.Key, userID)
	if err != nil {
		return domain.StatusResult{}, err
	}
	result.Progress = progress
	claims, err := s.repository.ListInviteActivityRewardClaimsByUserCycle(ctx, cycle.Key, userID)
	if err != nil {
		return domain.StatusResult{}, err
	}
	result.Claims = claims
	return result, nil
}

func (s *Service) GetConfig(ctx context.Context) (domain.Config, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Config{}, err
	}
	config, exists, err := s.repository.GetInviteActivityRewardConfig(ctx)
	if err != nil {
		return domain.Config{}, err
	}
	if !exists {
		return domain.Config{AppCode: appcode.FromContext(ctx), Enabled: true}, nil
	}
	return config, nil
}

func (s *Service) ListInvitees(ctx context.Context, userID int64, page int32, pageSize int32) (domain.InviteeResult, error) {
	if err := s.requireRepository(); err != nil {
		return domain.InviteeResult{}, err
	}
	if userID <= 0 {
		return domain.InviteeResult{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	repository, ok := s.repository.(InviteeRepository)
	if !ok {
		return domain.InviteeResult{}, xerr.New(xerr.Unavailable, "invitee repository is not configured")
	}
	if page <= 0 {
		page = 1
	}
	if pageSize <= 0 {
		pageSize = 50
	}
	if pageSize > 100 {
		pageSize = 100
	}
	now := s.now().UTC()
	cycle := CycleForTime(now)
	query := domain.InviteeQuery{
		UserID:   userID,
		CycleKey: cycle.Key,
		Page:     page,
		PageSize: pageSize,
	}
	// 查询条件由鉴权 user_id 和服务端 UTC 月份共同固定，App 不能借查询参数读取其他邀请人的明细。
	invitees, total, err := repository.ListInviteActivityRewardInvitees(ctx, query)
	if err != nil {
		return domain.InviteeResult{}, err
	}
	return domain.InviteeResult{
		Invitees:     invitees,
		Total:        total,
		Page:         page,
		PageSize:     pageSize,
		CycleKey:     cycle.Key,
		ServerTimeMS: now.UnixMilli(),
	}, nil
}

func (s *Service) UpdateConfig(ctx context.Context, config domain.Config) (domain.Config, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Config{}, err
	}
	config.AppCode = appcode.FromContext(ctx)
	config.Tiers = normalizeTiers(config.Tiers)
	if err := validateConfig(config); err != nil {
		return domain.Config{}, err
	}
	return s.repository.UpdateInviteActivityRewardConfig(ctx, config, s.now().UTC().UnixMilli())
}

func (s *Service) ConsumeWalletRechargeEvent(ctx context.Context, event domain.RechargeEvent) error {
	if err := s.requireRepository(); err != nil {
		return err
	}
	if event.EventType != domain.EventTypeWalletRechargeRecorded {
		return nil
	}
	event = normalizeRechargeEvent(event)
	if event.RechargeCoinAmount <= 0 || event.InvitedUserID <= 0 || event.EventID == "" {
		return nil
	}
	if s.invite == nil {
		return xerr.New(xerr.Unavailable, "invite attribution source is not configured")
	}
	// 邀请归因只由 user-service 拥有；activity-service 按充值事实反查邀请人，不复制邀请关系表。
	attribution, err := s.invite.GetInviteAttribution(appcode.WithContext(ctx, event.AppCode), event.InvitedUserID)
	if err != nil {
		return err
	}
	if attribution == nil || !attribution.GetFound() || attribution.GetInviterUserId() <= 0 {
		return nil
	}
	event.InviterUserID = attribution.GetInviterUserId()
	cycle := CycleForTime(time.UnixMilli(event.OccurredAtMS).UTC())
	_, err = s.repository.ConsumeInviteActivityRechargeEvent(appcode.WithContext(ctx, event.AppCode), event, cycle, s.now().UTC().UnixMilli())
	return err
}

func (s *Service) ConsumeValidInviteEvent(ctx context.Context, event domain.ValidInviteEvent) error {
	if err := s.requireRepository(); err != nil {
		return err
	}
	if event.EventType != domain.EventTypeUserInviteBecameValid || event.EventID == "" || event.InviterUserID <= 0 {
		return nil
	}
	if event.OccurredAtMS <= 0 {
		event.OccurredAtMS = s.now().UTC().UnixMilli()
	}
	cycle := CycleForTime(time.UnixMilli(event.OccurredAtMS).UTC())
	_, err := s.repository.ConsumeInviteActivityValidInviteEvent(appcode.WithContext(ctx, event.AppCode), event, cycle, s.now().UTC().UnixMilli())
	return err
}

func (s *Service) ConsumeInviteEvent(ctx context.Context, event domain.InviteEvent) error {
	if err := s.requireRepository(); err != nil {
		return err
	}
	if event.EventType != domain.EventTypeUserInvited || event.EventID == "" || event.InviterUserID <= 0 || event.InvitedUserID <= 0 {
		return nil
	}
	if event.OccurredAtMS <= 0 {
		event.OccurredAtMS = s.now().UTC().UnixMilli()
	}
	event.AppCode = appcode.Normalize(event.AppCode)
	event.InviteCode = strings.TrimSpace(event.InviteCode)
	event.Source = strings.TrimSpace(event.Source)
	cycle := CycleForTime(time.UnixMilli(event.OccurredAtMS).UTC())
	eventCtx := appcode.WithContext(ctx, event.AppCode)

	config, exists, err := s.repository.GetInviteActivityRewardConfig(eventCtx)
	if err != nil {
		return err
	}
	if !exists {
		config = domain.Config{AppCode: event.AppCode, Enabled: true}
	}
	if !config.Enabled {
		_, err := s.repository.RecordInviteActivityInviteEvent(eventCtx, event, cycle, "skipped", "invite activity is disabled", s.now().UTC().UnixMilli())
		return err
	}
	if config.PerInviteInviterRewardCoinAmount <= 0 && config.PerInviteInviteeRewardCoinAmount <= 0 {
		_, err := s.repository.RecordInviteActivityInviteEvent(eventCtx, event, cycle, "skipped", "per invite reward is not configured", s.now().UTC().UnixMilli())
		return err
	}
	if s.wallet == nil {
		return xerr.New(xerr.Unavailable, "wallet client is not configured")
	}
	// 事件表只做审计和去重观察；真正的钱包幂等落在 deterministic claim_id + wallet_command_id 上。
	// 因此 MQ 重试时即使事件已存在，也会继续复用同一 claim 重试未完成的钱包入账。
	if _, err := s.repository.RecordInviteActivityInviteEvent(eventCtx, event, cycle, "consumed", "", s.now().UTC().UnixMilli()); err != nil {
		return err
	}
	if config.PerInviteInviterRewardCoinAmount > 0 {
		claim := perInviteClaim(event, cycle, domain.RewardTypeInviteInviter, event.InviterUserID, event.InvitedUserID, config.PerInviteInviterRewardCoinAmount)
		if _, err := s.grantClaim(eventCtx, claim); err != nil {
			return err
		}
	}
	if config.PerInviteInviteeRewardCoinAmount > 0 {
		claim := perInviteClaim(event, cycle, domain.RewardTypeInvitee, event.InvitedUserID, event.InviterUserID, config.PerInviteInviteeRewardCoinAmount)
		if _, err := s.grantClaim(eventCtx, claim); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) Claim(ctx context.Context, userID int64, rewardType string, tierID int64, commandID string) (domain.Claim, domain.StatusResult, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Claim{}, domain.StatusResult{}, err
	}
	if s.wallet == nil {
		return domain.Claim{}, domain.StatusResult{}, xerr.New(xerr.Unavailable, "wallet client is not configured")
	}
	rewardType = normalizeTierRewardType(rewardType)
	commandID = strings.TrimSpace(commandID)
	if userID <= 0 || rewardType == "" || tierID <= 0 || commandID == "" {
		return domain.Claim{}, domain.StatusResult{}, xerr.New(xerr.InvalidArgument, "claim request is incomplete")
	}
	status, err := s.GetStatus(ctx, userID)
	if err != nil {
		return domain.Claim{}, domain.StatusResult{}, err
	}
	if !status.Config.Enabled {
		return domain.Claim{}, domain.StatusResult{}, xerr.New(xerr.Conflict, "invite activity is disabled")
	}
	tier, ok := findTier(status.Config.Tiers, rewardType, tierID)
	if !ok {
		return domain.Claim{}, domain.StatusResult{}, xerr.New(xerr.NotFound, "invite activity tier not found")
	}
	reached := reachedValue(status.Progress, rewardType)
	if !tierReached(tier, reached) {
		return domain.Claim{}, domain.StatusResult{}, xerr.New(xerr.Conflict, "invite activity tier is not reached")
	}
	rewardAmount, err := claimRewardAmount(status.Claims, status.Config.Tiers, tier, reached)
	if err != nil {
		return domain.Claim{}, domain.StatusResult{}, err
	}
	claim := domain.Claim{
		AppCode:                   appcode.FromContext(ctx),
		ClaimID:                   fmt.Sprintf("IAR_%d_%s_%s_%d", userID, status.Cycle.Key, rewardType, tier.TierID),
		CycleKey:                  status.Cycle.Key,
		UserID:                    userID,
		RewardType:                rewardType,
		CommandID:                 commandID,
		TierID:                    tier.TierID,
		TierCode:                  tier.TierCode,
		TierName:                  tier.TierName,
		ThresholdCoinAmount:       tier.ThresholdCoinAmount,
		ThresholdValidInviteCount: tier.ThresholdValidInviteCount,
		ReachedValue:              reached,
		RewardCoinAmount:          rewardAmount,
		WalletCommandID:           commandID,
	}
	updated, err := s.grantClaim(ctx, claim)
	if err != nil {
		return domain.Claim{}, domain.StatusResult{}, err
	}
	refreshed, err := s.GetStatus(ctx, userID)
	return updated, refreshed, err
}

func (s *Service) ListClaims(ctx context.Context, query domain.ClaimQuery) ([]domain.Claim, int64, error) {
	if err := s.requireRepository(); err != nil {
		return nil, 0, err
	}
	query.Status = strings.TrimSpace(query.Status)
	query.RewardType = normalizeRewardType(query.RewardType)
	query.CycleKey = strings.TrimSpace(query.CycleKey)
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = 20
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	return s.repository.ListInviteActivityRewardClaims(ctx, query)
}

func (s *Service) ListSummaries(ctx context.Context, query domain.SummaryQuery) ([]domain.Summary, int64, error) {
	if err := s.requireRepository(); err != nil {
		return nil, 0, err
	}
	query.CycleKey = strings.TrimSpace(query.CycleKey)
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = 20
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	// 后台聚合列表必须以服务端事实表分页，避免前端只合并当前页 claim 后漏掉未发奖但已邀请的人。
	return s.repository.ListInviteActivityRewardSummaries(ctx, query)
}

func (s *Service) ListLeaderboard(ctx context.Context, query domain.LeaderboardQuery) (domain.LeaderboardResult, error) {
	if err := s.requireRepository(); err != nil {
		return domain.LeaderboardResult{}, err
	}
	now := s.now().UTC()
	query.CycleKey = strings.TrimSpace(query.CycleKey)
	if query.CycleKey == "" {
		query.CycleKey = CycleForTime(now).Key
	}
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = 50
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	// 邀请活动榜单只对外展示“有效邀请人数”榜，累计充值金额只作为同分时的稳定排序依据和扩展字段返回。
	entries, total, err := s.repository.ListInviteActivityLeaderboard(ctx, query)
	if err != nil {
		return domain.LeaderboardResult{}, err
	}
	return domain.LeaderboardResult{
		Entries:      entries,
		Total:        total,
		CycleKey:     query.CycleKey,
		ServerTimeMS: now.UnixMilli(),
	}, nil
}

func CycleForTime(value time.Time) domain.Cycle {
	utc := value.UTC()
	start := time.Date(utc.Year(), utc.Month(), 1, 0, 0, 0, 0, time.UTC)
	end := start.AddDate(0, 1, 0)
	return domain.Cycle{Key: start.Format("2006-01"), StartMS: start.UnixMilli(), EndMS: end.UnixMilli() - 1}
}

func normalizeTiers(tiers []domain.Tier) []domain.Tier {
	items := make([]domain.Tier, 0, len(tiers))
	for _, tier := range tiers {
		tier.RewardType = normalizeTierRewardType(tier.RewardType)
		tier.TierCode = strings.TrimSpace(tier.TierCode)
		tier.TierName = strings.TrimSpace(tier.TierName)
		tier.Status = strings.ToLower(strings.TrimSpace(tier.Status))
		if tier.Status == "" {
			tier.Status = domain.TierStatusActive
		}
		items = append(items, tier)
	}
	sort.SliceStable(items, func(i, j int) bool {
		if items[i].RewardType == items[j].RewardType {
			if items[i].SortOrder == items[j].SortOrder {
				return thresholdValue(items[i]) < thresholdValue(items[j])
			}
			return items[i].SortOrder > items[j].SortOrder
		}
		return items[i].RewardType < items[j].RewardType
	})
	return items
}

func validateConfig(config domain.Config) error {
	if config.UpdatedByAdminID <= 0 {
		return xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
	}
	if config.PerInviteInviterRewardCoinAmount < 0 || config.PerInviteInviteeRewardCoinAmount < 0 {
		return xerr.New(xerr.InvalidArgument, "per invite reward coin amount must not be negative")
	}
	if config.Enabled && len(activeTiers(config.Tiers)) == 0 && config.PerInviteInviterRewardCoinAmount <= 0 && config.PerInviteInviteeRewardCoinAmount <= 0 {
		return xerr.New(xerr.InvalidArgument, "enabled invite activity requires active tiers or per invite rewards")
	}
	seen := map[string]struct{}{}
	for _, tier := range config.Tiers {
		if tier.RewardType == "" || tier.TierCode == "" || tier.RewardCoinAmount <= 0 {
			return xerr.New(xerr.InvalidArgument, "invite activity tier is incomplete")
		}
		if tier.RewardType == domain.RewardTypeRecharge && tier.ThresholdCoinAmount <= 0 {
			return xerr.New(xerr.InvalidArgument, "recharge tier threshold is required")
		}
		if tier.RewardType == domain.RewardTypeValidInvite && tier.ThresholdValidInviteCount <= 0 {
			return xerr.New(xerr.InvalidArgument, "valid invite tier threshold is required")
		}
		key := fmt.Sprintf("%s:%d", tier.RewardType, thresholdValue(tier))
		if _, ok := seen[key]; ok {
			return xerr.New(xerr.InvalidArgument, "duplicate invite activity tier threshold")
		}
		seen[key] = struct{}{}
	}
	return nil
}

func (s *Service) grantClaim(ctx context.Context, claim domain.Claim) (domain.Claim, error) {
	created, err := s.repository.CreateInviteActivityRewardClaim(ctx, claim, s.now().UTC().UnixMilli())
	if err != nil {
		return domain.Claim{}, err
	}
	if created.Status == domain.ClaimStatusGranted {
		return created, nil
	}
	// 钱包 command_id 与 claim_id 都是稳定值；失败重试会复用同一 claim，钱包已成功但回包丢失时会由钱包幂等回放交易。
	resp, err := s.wallet.CreditInviteActivityReward(ctx, &walletv1.CreditInviteActivityRewardRequest{
		CommandId:    created.WalletCommandID,
		AppCode:      appcode.FromContext(ctx),
		TargetUserId: created.UserID,
		Amount:       created.RewardCoinAmount,
		ClaimId:      created.ClaimID,
		RewardType:   created.RewardType,
		TierId:       created.TierID,
		TierCode:     created.TierCode,
		CycleKey:     created.CycleKey,
		ReachedValue: created.ReachedValue,
		Reason:       grantReason,
	})
	if err != nil {
		_ = s.repository.MarkInviteActivityRewardClaimFailed(ctx, created.ClaimID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
		return domain.Claim{}, err
	}
	return s.repository.MarkInviteActivityRewardClaimGranted(ctx, created.ClaimID, resp.GetTransactionId(), resp.GetGrantedAtMs())
}

func perInviteClaim(event domain.InviteEvent, cycle domain.Cycle, rewardType string, userID int64, peerUserID int64, amount int64) domain.Claim {
	claimID := perInviteClaimID(event, rewardType)
	return domain.Claim{
		AppCode:                   event.AppCode,
		ClaimID:                   claimID,
		CycleKey:                  cycle.Key,
		UserID:                    userID,
		RewardType:                rewardType,
		CommandID:                 "wallet_" + claimID,
		TierID:                    peerUserID,
		TierCode:                  rewardType,
		TierName:                  perInviteTierName(rewardType),
		ThresholdCoinAmount:       0,
		ThresholdValidInviteCount: 0,
		ReachedValue:              1,
		RewardCoinAmount:          amount,
		WalletCommandID:           "wallet_" + claimID,
	}
}

func perInviteClaimID(event domain.InviteEvent, rewardType string) string {
	sum := sha1.Sum([]byte(fmt.Sprintf("%s|%s|%s|%d|%d", appcode.Normalize(event.AppCode), strings.TrimSpace(event.EventID), rewardType, event.InviterUserID, event.InvitedUserID)))
	return fmt.Sprintf("IAR_INVITE_%s", hex.EncodeToString(sum[:])[:32])
}

func perInviteTierName(rewardType string) string {
	if rewardType == domain.RewardTypeInvitee {
		return "每邀请一人被邀请人奖励"
	}
	return "每邀请一人邀请人奖励"
}

func claimRewardAmount(claims []domain.Claim, tiers []domain.Tier, tier domain.Tier, reached int64) (int64, error) {
	if tier.RewardType == domain.RewardTypeValidInvite {
		for _, claim := range claims {
			if claim.RewardType == tier.RewardType && claim.TierID == tier.TierID && claim.Status == domain.ClaimStatusGranted {
				return 0, xerr.New(xerr.Conflict, "invite activity tier already claimed")
			}
		}
		return tier.RewardCoinAmount, nil
	}
	highest, ok := highestReachedTier(tiers, domain.RewardTypeRecharge, reached)
	if !ok || highest.TierID != tier.TierID {
		return 0, xerr.New(xerr.Conflict, "only highest recharge tier can be claimed")
	}
	var granted int64
	for _, claim := range claims {
		if claim.RewardType == domain.RewardTypeRecharge && claim.Status == domain.ClaimStatusGranted {
			granted += claim.RewardCoinAmount
		}
	}
	amount := tier.RewardCoinAmount - granted
	if amount <= 0 {
		return 0, xerr.New(xerr.Conflict, "invite activity reward already claimed")
	}
	return amount, nil
}

func activeTiers(tiers []domain.Tier) []domain.Tier {
	items := make([]domain.Tier, 0, len(tiers))
	for _, tier := range tiers {
		if tier.Status == domain.TierStatusActive {
			items = append(items, tier)
		}
	}
	return items
}

func normalizeRewardType(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case domain.RewardTypeRecharge:
		return domain.RewardTypeRecharge
	case domain.RewardTypeValidInvite:
		return domain.RewardTypeValidInvite
	case domain.RewardTypeInviteInviter:
		return domain.RewardTypeInviteInviter
	case domain.RewardTypeInvitee:
		return domain.RewardTypeInvitee
	default:
		return ""
	}
}

func normalizeTierRewardType(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case domain.RewardTypeRecharge:
		return domain.RewardTypeRecharge
	case domain.RewardTypeValidInvite:
		return domain.RewardTypeValidInvite
	default:
		return ""
	}
}

func normalizeRechargeEvent(event domain.RechargeEvent) domain.RechargeEvent {
	event.AppCode = appcode.Normalize(event.AppCode)
	event.EventID = strings.TrimSpace(event.EventID)
	event.EventType = strings.TrimSpace(event.EventType)
	event.TransactionID = strings.TrimSpace(event.TransactionID)
	event.CommandID = strings.TrimSpace(event.CommandID)
	event.RechargeType = strings.TrimSpace(event.RechargeType)
	if event.OccurredAtMS <= 0 {
		event.OccurredAtMS = time.Now().UTC().UnixMilli()
	}
	return event
}

func reachedValue(progress domain.Progress, rewardType string) int64 {
	if rewardType == domain.RewardTypeValidInvite {
		return progress.ValidInviteCount
	}
	return progress.TotalRechargeCoinAmount
}

func tierReached(tier domain.Tier, reached int64) bool {
	return reached >= thresholdValue(tier)
}

func thresholdValue(tier domain.Tier) int64 {
	if tier.RewardType == domain.RewardTypeValidInvite {
		return tier.ThresholdValidInviteCount
	}
	return tier.ThresholdCoinAmount
}

func findTier(tiers []domain.Tier, rewardType string, tierID int64) (domain.Tier, bool) {
	for _, tier := range tiers {
		if tier.RewardType == rewardType && tier.TierID == tierID {
			return tier, true
		}
	}
	return domain.Tier{}, false
}

func highestReachedTier(tiers []domain.Tier, rewardType string, reached int64) (domain.Tier, bool) {
	var selected domain.Tier
	for _, tier := range tiers {
		if tier.RewardType != rewardType || !tierReached(tier, reached) {
			continue
		}
		if selected.TierID == 0 || thresholdValue(tier) > thresholdValue(selected) {
			selected = tier
		}
	}
	return selected, selected.TierID > 0
}

func (s *Service) requireRepository() error {
	if s == nil || s.repository == nil {
		return xerr.New(xerr.Unavailable, "invite activity repository is not configured")
	}
	return nil
}
