package luckygift

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"
	"sync"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/tencentim"
	"hyapp/pkg/xerr"
	broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
	domain "hyapp/services/activity-service/internal/domain/luckygift"
	broadcastservice "hyapp/services/activity-service/internal/service/broadcast"

	"google.golang.org/grpc"
	walletv1 "hyapp.local/api/proto/wallet/v1"
)

// Repository 是幸运礼物唯一持久化边界；RTP、奖池、风控计数必须在同一个 MySQL 事务内修改。
type Repository interface {
	GetLuckyGiftRuleConfig(ctx context.Context, poolID string) (domain.RuleConfig, bool, error)
	PublishLuckyGiftRuleConfig(ctx context.Context, config domain.RuleConfig, nowMS int64) (domain.RuleConfig, error)
	ListLuckyGiftRuleConfigs(ctx context.Context) ([]domain.RuleConfig, error)
	CheckLuckyGift(ctx context.Context, cmd domain.CheckCommand) (domain.CheckResult, error)
	ExecuteLuckyGiftDraw(ctx context.Context, cmd domain.DrawCommand, nowMS int64) (domain.DrawResult, error)
	ListLuckyGiftDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error)
	GetLuckyGiftDrawSummary(ctx context.Context, query domain.DrawQuery) (domain.DrawSummary, error)
	GetLuckyGiftDrawRewardState(ctx context.Context, appCode string, drawIDs []string) (domain.DrawRewardState, error)
	ClaimPendingLuckyGiftOutbox(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int) ([]domain.DrawOutbox, error)
	MarkLuckyGiftOutboxDelivered(ctx context.Context, event domain.DrawOutbox, nowMS int64) error
	MarkLuckyGiftOutboxRetryable(ctx context.Context, event domain.DrawOutbox, retryCount int, nextRetryAtMS int64, lastErr string, nowMS int64) error
	MarkLuckyGiftOutboxFailed(ctx context.Context, event domain.DrawOutbox, retryCount int, lastErr string, nowMS int64) error
	RefreshLuckyGiftAdminStatsSnapshots(ctx context.Context, nowMS int64, batchSize int) (int, error)
	RefreshLuckyGiftDatabiStatsSnapshots(ctx context.Context, nowMS int64, batchSize int) (int, error)
	MarkLuckyGiftDrawGranted(ctx context.Context, appCode string, drawID string, walletTransactionID string, nowMS int64) error
	MarkLuckyGiftDrawFailed(ctx context.Context, appCode string, drawID string, failureReason string, nowMS int64) error
	// 批量抽奖只产生一条聚合 outbox；worker 处理后需要用 draw_ids 一次性收敛所有明细状态。
	MarkLuckyGiftDrawsGranted(ctx context.Context, appCode string, drawIDs []string, walletTransactionID string, nowMS int64) error
	MarkLuckyGiftDrawsFailed(ctx context.Context, appCode string, drawIDs []string, failureReason string, nowMS int64) error
}

// WalletClient 是幸运礼物返奖唯一账务依赖；实际余额变更通知继续由 wallet outbox -> notice-service 投递。
type WalletClient interface {
	CreditLuckyGiftReward(ctx context.Context, req *walletv1.CreditLuckyGiftRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditLuckyGiftRewardResponse, error)
}

// RoomPublisher 只负责投递房间群自定义消息，不拥有房间状态。
type RoomPublisher interface {
	PublishGroupCustomMessage(ctx context.Context, message tencentim.CustomGroupMessage) error
}

// RegionBroadcaster 只写 activity-service 自有的区域播报 outbox；真正发腾讯 IM 仍由 broadcast worker 补偿。
type RegionBroadcaster interface {
	PublishRegionBroadcast(ctx context.Context, input broadcastservice.PublishInput) (broadcastdomain.PublishResult, error)
}

// Service 承载幸运礼物查询、抽奖命令和后台规则配置。
type Service struct {
	repository  Repository
	wallet      WalletClient
	publisher   RoomPublisher
	broadcaster RegionBroadcaster
	now         func() time.Time
}

type Option func(*Service)

func WithWallet(client WalletClient) Option {
	return func(s *Service) {
		s.wallet = client
	}
}

func WithRoomPublisher(publisher RoomPublisher) Option {
	return func(s *Service) {
		s.publisher = publisher
	}
}

func WithRegionBroadcaster(broadcaster RegionBroadcaster) Option {
	return func(s *Service) {
		s.broadcaster = broadcaster
	}
}

func New(repository Repository, options ...Option) *Service {
	service := &Service{repository: repository, now: time.Now}
	for _, option := range options {
		if option != nil {
			option(service)
		}
	}
	return service
}

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

func (s *Service) Check(ctx context.Context, cmd domain.CheckCommand) (domain.CheckResult, error) {
	if err := s.requireRepository(); err != nil {
		return domain.CheckResult{}, err
	}
	// Check 是送礼前拦截，不产生抽奖事实；这里只归一化入参，最终启停仍以仓储中的当前规则为准。
	cmd.PoolID = normalizePoolID(cmd.PoolID)
	cmd.GiftID = strings.TrimSpace(cmd.GiftID)
	cmd.RoomID = strings.TrimSpace(cmd.RoomID)
	if cmd.GiftID == "" || cmd.UserID <= 0 || cmd.RoomID == "" {
		return domain.CheckResult{}, xerr.New(xerr.InvalidArgument, "lucky gift check command is incomplete")
	}
	return s.repository.CheckLuckyGift(ctx, cmd)
}

func (s *Service) Draw(ctx context.Context, cmd domain.DrawCommand) (domain.DrawResult, error) {
	if err := s.requireRepository(); err != nil {
		return domain.DrawResult{}, err
	}
	// Draw 只接受扣费成功后的服务端事实；客户端不能提交价格、倍率、奖励或风控绕过字段。
	cmd.CommandID = strings.TrimSpace(cmd.CommandID)
	cmd.PoolID = normalizePoolID(cmd.PoolID)
	cmd.GiftID = strings.TrimSpace(cmd.GiftID)
	cmd.DeviceID = strings.TrimSpace(cmd.DeviceID)
	cmd.RoomID = strings.TrimSpace(cmd.RoomID)
	cmd.AnchorID = strings.TrimSpace(cmd.AnchorID)
	if cmd.CommandID == "" || cmd.UserID <= 0 || cmd.DeviceID == "" || cmd.RoomID == "" || cmd.AnchorID == "" || cmd.GiftID == "" || cmd.CoinSpent <= 0 {
		return domain.DrawResult{}, xerr.New(xerr.InvalidArgument, "lucky gift draw command is incomplete")
	}
	if cmd.TargetUserID < 0 || cmd.GiftCount <= 0 {
		return domain.DrawResult{}, xerr.New(xerr.InvalidArgument, "lucky gift draw metadata is invalid")
	}
	if cmd.PaidAtMS <= 0 {
		// paid_at_ms 是扣费事实时间，缺失时用 UTC 服务端时间补齐，避免使用本地时区切风控窗口。
		cmd.PaidAtMS = s.now().UTC().UnixMilli()
	}
	result, err := s.repository.ExecuteLuckyGiftDraw(ctx, cmd, s.now().UTC().UnixMilli())
	if err != nil {
		return domain.DrawResult{}, err
	}
	return s.creditLuckyGiftRewardFastPath(ctx, cmd, result), nil
}

type WorkerOptions struct {
	WorkerID       string
	PollInterval   time.Duration
	BatchSize      int
	Concurrency    int
	LockTTL        time.Duration
	MaxRetry       int
	PublishTimeout time.Duration
	StatsInterval  time.Duration
	StatsBatchSize int
}

func (s *Service) RunWorker(ctx context.Context, options WorkerOptions) {
	options = normalizeWorkerOptions(options)
	ticker := time.NewTicker(options.PollInterval)
	defer ticker.Stop()
	nextStatsRefresh := time.Time{}
	for {
		if _, err := s.ProcessPendingDrawOutbox(ctx, options); err != nil && ctx.Err() == nil {
			// 批处理失败不删除 outbox；锁过期后下一轮或其他实例会继续补偿。
			logx.Error(ctx, "lucky_gift_outbox_batch_failed", err, slog.String("worker_id", options.WorkerID))
		}
		now := s.now().UTC()
		if options.StatsInterval > 0 && (nextStatsRefresh.IsZero() || !now.Before(nextStatsRefresh)) {
			if _, err := s.repository.RefreshLuckyGiftAdminStatsSnapshots(ctx, now.UnixMilli(), options.StatsBatchSize); err != nil && ctx.Err() == nil {
				logx.Error(ctx, "lucky_gift_admin_stats_refresh_failed", err, slog.String("worker_id", options.WorkerID))
			}
			if _, err := s.repository.RefreshLuckyGiftDatabiStatsSnapshots(ctx, now.UnixMilli(), options.StatsBatchSize); err != nil && ctx.Err() == nil {
				logx.Error(ctx, "lucky_gift_databi_stats_refresh_failed", err, slog.String("worker_id", options.WorkerID))
			}
			nextStatsRefresh = now.Add(options.StatsInterval)
		}
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
		}
	}
}

func (s *Service) ProcessPendingDrawOutbox(ctx context.Context, options WorkerOptions) (int, error) {
	if err := s.requireRepository(); err != nil {
		return 0, err
	}
	options = normalizeWorkerOptions(options)
	// outbox 只承载钱包返奖结算，避免多实例同时给同一 draw 返奖。
	events, err := s.repository.ClaimPendingLuckyGiftOutbox(ctx, options.WorkerID, s.now().UTC().UnixMilli(), options.LockTTL, options.BatchSize)
	if err != nil {
		return 0, err
	}
	if len(events) == 0 {
		return 0, nil
	}
	workerCount := options.Concurrency
	if workerCount > len(events) {
		workerCount = len(events)
	}
	jobs := make(chan domain.DrawOutbox)
	errs := make(chan error, len(events))
	var wg sync.WaitGroup
	for index := 0; index < workerCount; index++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for event := range jobs {
				if err := s.processDrawOutbox(ctx, event, options); err != nil {
					errs <- err
					continue
				}
				errs <- nil
			}
		}()
	}
	for _, event := range events {
		select {
		case <-ctx.Done():
			close(jobs)
			wg.Wait()
			close(errs)
			return 0, ctx.Err()
		case jobs <- event:
		}
	}
	close(jobs)
	wg.Wait()
	close(errs)
	processed := 0
	var firstErr error
	for err := range errs {
		if err != nil {
			if firstErr == nil {
				firstErr = err
			}
			continue
		}
		processed++
	}
	return processed, firstErr
}

func (s *Service) processDrawOutbox(ctx context.Context, event domain.DrawOutbox, options WorkerOptions) error {
	payload, err := decodeDrawOutboxPayload(event.PayloadJSON)
	if err != nil {
		// payload 不可解析说明抽奖事实旁路已经损坏，重试也无法恢复，直接走失败收敛。
		return s.markOutboxFailed(ctx, event, options, err, false)
	}
	eventCtx := appcode.WithContext(ctx, event.AppCode)
	switch event.EventType {
	case domain.EventTypeLuckyGiftRewardSettlement, domain.EventTypeLuckyGiftDrawn, "":
		return s.processRewardSettlementOutbox(eventCtx, event, payload, options)
	default:
		return s.markOutboxFailed(eventCtx, event, options, fmt.Errorf("unsupported lucky gift outbox event type: %s", event.EventType), false)
	}
}

func (s *Service) processRewardSettlementOutbox(ctx context.Context, event domain.DrawOutbox, payload luckyGiftDrawnPayload, options WorkerOptions) error {
	if _, _, err := s.settleLuckyGiftReward(ctx, event, payload, options); err != nil {
		return err
	}
	return s.repository.MarkLuckyGiftOutboxDelivered(ctx, event, s.now().UTC().UnixMilli())
}

func (s *Service) settleLuckyGiftReward(ctx context.Context, event domain.DrawOutbox, payload luckyGiftDrawnPayload, options WorkerOptions) (bool, bool, error) {
	walletTransactionID := ""
	credited := false
	alreadyGranted := false
	if payload.EffectiveRewardCoins > 0 {
		state, err := s.repository.GetLuckyGiftDrawRewardState(ctx, event.AppCode, payload.DrawIDs)
		if err != nil {
			return false, false, s.markOutboxFailed(ctx, event, options, err, false)
		}
		alreadyGranted = state.AllGranted
		if alreadyGranted {
			walletTransactionID = state.WalletTransactionID
			credited = true
		} else {
			receipt, err := s.creditLuckyGiftReward(ctx, event.AppCode, payload)
			if err != nil {
				return false, false, s.markOutboxFailed(ctx, event, options, err, false)
			}
			walletTransactionID = receipt.WalletTransactionID
			credited = true
		}
	}
	if !credited {
		if err := s.markLuckyGiftDrawsGrantedAfterWallet(ctx, event.AppCode, payload.DrawIDs, walletTransactionID); err != nil {
			return false, false, err
		}
		return false, false, nil
	}
	if !alreadyGranted {
		if err := s.markLuckyGiftDrawsGrantedAfterWallet(ctx, event.AppCode, payload.DrawIDs, walletTransactionID); err != nil {
			return credited, alreadyGranted, err
		}
	}
	return credited, alreadyGranted, nil
}

type luckyGiftRewardReceipt struct {
	WalletTransactionID string
	CoinBalanceAfter    int64
}

func (s *Service) creditLuckyGiftRewardFastPath(ctx context.Context, cmd domain.DrawCommand, result domain.DrawResult) domain.DrawResult {
	if result.EffectiveRewardCoins <= 0 || result.RewardStatus == domain.StatusGranted {
		return result
	}
	payload := luckyGiftDrawnPayload{
		AppCode:                   appcode.FromContext(ctx),
		DrawID:                    result.DrawID,
		DrawIDs:                   luckyDrawResultIDs(result),
		CommandID:                 cmd.CommandID,
		PoolID:                    result.PoolID,
		RoomID:                    cmd.RoomID,
		GiftID:                    cmd.GiftID,
		GiftCount:                 cmd.GiftCount,
		UserID:                    cmd.UserID,
		SenderUserID:              cmd.UserID,
		TargetUserID:              cmd.TargetUserID,
		VisibleRegionID:           cmd.VisibleRegionID,
		CoinSpent:                 cmd.CoinSpent,
		RuleVersion:               result.RuleVersion,
		ExperiencePool:            result.ExperiencePool,
		SelectedTierID:            result.SelectedTierID,
		MultiplierPPM:             result.MultiplierPPM,
		BaseRewardCoins:           result.BaseRewardCoins,
		RoomAtmosphereRewardCoins: result.RoomAtmosphereRewardCoins,
		ActivitySubsidyCoins:      result.ActivitySubsidyCoins,
		EffectiveRewardCoins:      result.EffectiveRewardCoins,
		StageFeedback:             result.StageFeedback,
		HighMultiplier:            result.HighMultiplier,
		CreatedAtMS:               result.CreatedAtMS,
	}
	receipt, err := s.creditLuckyGiftReward(ctx, payload.AppCode, payload)
	if err != nil {
		logx.Error(ctx, "lucky_gift_reward_fast_path_failed", err,
			slog.String("draw_id", result.DrawID),
			slog.String("command_id", result.CommandID),
			slog.Int64("reward_coins", result.EffectiveRewardCoins),
		)
		return result
	}
	result.RewardStatus = domain.StatusGranted
	result.WalletTransactionID = receipt.WalletTransactionID
	result.CoinBalanceAfter = receipt.CoinBalanceAfter
	if err := s.markLuckyGiftDrawsGrantedAfterWallet(ctx, payload.AppCode, payload.DrawIDs, receipt.WalletTransactionID); err != nil {
		logx.Error(ctx, "lucky_gift_reward_fast_path_mark_granted_failed", err,
			slog.String("draw_id", result.DrawID),
			slog.String("command_id", result.CommandID),
			slog.String("wallet_transaction_id", receipt.WalletTransactionID),
		)
	}
	return result
}

func (s *Service) markLuckyGiftDrawsGrantedAfterWallet(ctx context.Context, appCode string, drawIDs []string, walletTransactionID string) error {
	baseCtx := context.WithoutCancel(ctx)
	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		markCtx, cancel := context.WithTimeout(appcode.WithContext(baseCtx, appCode), 5*time.Second)
		err := s.repository.MarkLuckyGiftDrawsGranted(markCtx, appCode, drawIDs, walletTransactionID, s.now().UTC().UnixMilli())
		cancel()
		if err == nil {
			return nil
		}
		lastErr = err
		if !isRetryableLuckyGrantMarkError(err) {
			return err
		}
		time.Sleep(time.Duration(attempt+1) * 50 * time.Millisecond)
	}
	return lastErr
}

func (s *Service) creditLuckyGiftReward(ctx context.Context, appCode string, payload luckyGiftDrawnPayload) (luckyGiftRewardReceipt, error) {
	if s.wallet == nil {
		return luckyGiftRewardReceipt{}, fmt.Errorf("lucky gift wallet client is not configured")
	}
	resp, err := s.wallet.CreditLuckyGiftReward(appcode.WithContext(ctx, appCode), &walletv1.CreditLuckyGiftRewardRequest{
		CommandId:       "lucky_reward:" + payload.DrawID,
		AppCode:         appCode,
		TargetUserId:    payload.UserID,
		Amount:          payload.EffectiveRewardCoins,
		DrawId:          payload.DrawID,
		RoomId:          payload.RoomID,
		GiftId:          payload.GiftID,
		PoolId:          payload.PoolID,
		Reason:          "lucky_gift_reward",
		VisibleRegionId: payload.VisibleRegionID,
	})
	if err != nil {
		return luckyGiftRewardReceipt{}, err
	}
	return luckyGiftRewardReceipt{
		WalletTransactionID: resp.GetTransactionId(),
		CoinBalanceAfter:    resp.GetBalance().GetAvailableAmount(),
	}, nil
}

func (s *Service) publishLuckyGiftDrawn(ctx context.Context, payload luckyGiftDrawnPayload, options WorkerOptions) error {
	body, err := json.Marshal(luckyGiftRoomMessagePayload(payload))
	if err != nil {
		return err
	}
	// 房间 IM 只是表现层事实同步，不参与钱包结算和 reward_status 收敛。
	publishCtx, cancel := context.WithTimeout(ctx, options.PublishTimeout)
	defer cancel()
	return s.publisher.PublishGroupCustomMessage(publishCtx, tencentim.CustomGroupMessage{
		GroupID:     payload.RoomID,
		EventID:     payload.EventID,
		Desc:        "lucky_gift_drawn",
		Ext:         "room_system_message",
		PayloadJSON: body,
	})
}

func (s *Service) publishLuckyGiftRegionBroadcast(ctx context.Context, payload luckyGiftDrawnPayload) error {
	if payload.VisibleRegionID <= 0 {
		return nil
	}
	body, err := json.Marshal(luckyGiftRegionBroadcastPayload(payload, s.now().UTC().UnixMilli()))
	if err != nil {
		return err
	}
	_, err = s.broadcaster.PublishRegionBroadcast(ctx, broadcastservice.PublishInput{
		EventID:       "lucky_gift_big_win:" + payload.DrawID,
		BroadcastType: broadcastdomain.TypeLuckyGiftBigWin,
		RegionID:      payload.VisibleRegionID,
		PayloadJSON:   string(body),
	})
	return err
}

func (s *Service) markOutboxFailed(ctx context.Context, event domain.DrawOutbox, options WorkerOptions, cause error, alreadyCredited bool) error {
	nowMS := s.now().UTC().UnixMilli()
	nextRetryCount := event.RetryCount + 1
	if nextRetryCount >= options.MaxRetry {
		if !alreadyCredited {
			// 未返奖的 draw 可以整批标记 failed；钱包已返奖时不能覆盖账务事实。
			_ = s.repository.MarkLuckyGiftDrawsFailed(ctx, event.AppCode, drawIDsFromPayload(event.PayloadJSON), cause.Error(), nowMS)
		}
		return s.repository.MarkLuckyGiftOutboxFailed(ctx, event, nextRetryCount, cause.Error(), nowMS)
	}
	// 指数退避降低故障期间对钱包的压力，同时保留 pending 状态给客户端展示“处理中”。
	nextRetryAtMS := nowMS + luckyGiftBackoff(nextRetryCount).Milliseconds()
	return s.repository.MarkLuckyGiftOutboxRetryable(ctx, event, nextRetryCount, nextRetryAtMS, cause.Error(), nowMS)
}

func (s *Service) GetConfig(ctx context.Context, poolID string) (domain.RuleConfig, error) {
	if err := s.requireRepository(); err != nil {
		return domain.RuleConfig{}, err
	}
	poolID = normalizePoolID(poolID)
	config, exists, err := s.repository.GetLuckyGiftRuleConfig(ctx, poolID)
	if err != nil {
		return domain.RuleConfig{}, err
	}
	if !exists {
		// 未配置时返回 disabled v2 草稿，后台可以直接编辑发布；运行抽奖不会读取这个草稿。
		return DefaultRuleConfig(appcode.FromContext(ctx), poolID), nil
	}
	return config, nil
}

func (s *Service) UpsertConfig(ctx context.Context, config domain.RuleConfig) (domain.RuleConfig, error) {
	if err := s.requireRepository(); err != nil {
		return domain.RuleConfig{}, err
	}
	config.AppCode = appcode.FromContext(ctx)
	config.PoolID = normalizePoolID(config.PoolID)
	config = normalizeRuleConfig(config)
	if err := validateRuleConfig(config); err != nil {
		return domain.RuleConfig{}, err
	}
	return s.repository.PublishLuckyGiftRuleConfig(ctx, config, s.now().UTC().UnixMilli())
}

func (s *Service) ListConfigs(ctx context.Context) ([]domain.RuleConfig, error) {
	if err := s.requireRepository(); err != nil {
		return nil, err
	}
	return s.repository.ListLuckyGiftRuleConfigs(ctx)
}

func (s *Service) ListDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error) {
	if err := s.requireRepository(); err != nil {
		return nil, 0, err
	}
	query = normalizeDrawQuery(query)
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = 20
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	return s.repository.ListLuckyGiftDraws(ctx, query)
}

func (s *Service) GetDrawSummary(ctx context.Context, query domain.DrawQuery) (domain.DrawSummary, error) {
	if err := s.requireRepository(); err != nil {
		return domain.DrawSummary{}, err
	}
	return s.repository.GetLuckyGiftDrawSummary(ctx, normalizeDrawQuery(query))
}

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

func normalizeDrawQuery(query domain.DrawQuery) domain.DrawQuery {
	query.PoolID = normalizePoolID(query.PoolID)
	query.GiftID = strings.TrimSpace(query.GiftID)
	query.RoomID = strings.TrimSpace(query.RoomID)
	query.Status = strings.TrimSpace(query.Status)
	return query
}

type luckyGiftDrawnPayload struct {
	EventType string `json:"event_type"`
	EventID   string `json:"event_id"`
	AppCode   string `json:"app_code"`
	DrawID    string `json:"draw_id"`
	// DrawIDs 是聚合发放的明细集合；老 outbox 没有该字段时 decode 会回填为单个 DrawID。
	DrawIDs                   []string `json:"draw_ids,omitempty"`
	CommandID                 string   `json:"command_id"`
	PoolID                    string   `json:"pool_id"`
	RoomID                    string   `json:"room_id"`
	GiftID                    string   `json:"gift_id"`
	GiftCount                 int32    `json:"gift_count"`
	UserID                    int64    `json:"user_id"`
	SenderUserID              int64    `json:"sender_user_id"`
	TargetUserID              int64    `json:"target_user_id"`
	VisibleRegionID           int64    `json:"visible_region_id"`
	CoinSpent                 int64    `json:"coin_spent"`
	RuleVersion               int64    `json:"rule_version"`
	ExperiencePool            string   `json:"experience_pool"`
	SelectedTierID            string   `json:"selected_tier_id"`
	MultiplierPPM             int64    `json:"multiplier_ppm"`
	BaseRewardCoins           int64    `json:"base_reward_coins"`
	RoomAtmosphereRewardCoins int64    `json:"room_atmosphere_reward_coins"`
	ActivitySubsidyCoins      int64    `json:"activity_subsidy_coins"`
	EffectiveRewardCoins      int64    `json:"effective_reward_coins"`
	StageFeedback             bool     `json:"stage_feedback"`
	HighMultiplier            bool     `json:"high_multiplier"`
	CreatedAtMS               int64    `json:"created_at_ms"`
}

func shouldPublishLuckyGiftRegionBroadcast(payload luckyGiftDrawnPayload) bool {
	// 当前产品策略先把 1 倍作为默认区域飘屏阈值；后续接后台配置时只替换这里的阈值来源。
	return payload.VisibleRegionID > 0 && payload.EffectiveRewardCoins > 0 && payload.MultiplierPPM >= 1_000_000
}

func luckyGiftRoomMessagePayload(payload luckyGiftDrawnPayload) map[string]any {
	return map[string]any{
		"event_type":                   "lucky_gift_drawn",
		"event_id":                     payload.EventID,
		"app_code":                     payload.AppCode,
		"draw_id":                      payload.DrawID,
		"command_id":                   payload.CommandID,
		"pool_id":                      payload.PoolID,
		"room_id":                      payload.RoomID,
		"gift_id":                      payload.GiftID,
		"gift_count":                   payload.GiftCount,
		"user_id":                      payload.UserID,
		"sender_user_id":               payload.SenderUserID,
		"target_user_id":               payload.TargetUserID,
		"visible_region_id":            payload.VisibleRegionID,
		"coin_spent":                   payload.CoinSpent,
		"rule_version":                 payload.RuleVersion,
		"experience_pool":              payload.ExperiencePool,
		"selected_tier_id":             payload.SelectedTierID,
		"multiplier_ppm":               payload.MultiplierPPM,
		"base_reward_coins":            payload.BaseRewardCoins,
		"room_atmosphere_reward_coins": payload.RoomAtmosphereRewardCoins,
		"activity_subsidy_coins":       payload.ActivitySubsidyCoins,
		"effective_reward_coins":       payload.EffectiveRewardCoins,
		"stage_feedback":               payload.StageFeedback,
		"high_multiplier":              payload.HighMultiplier,
		"created_at_ms":                payload.CreatedAtMS,
	}
}

func luckyGiftRegionBroadcastPayload(payload luckyGiftDrawnPayload, sentAtMS int64) map[string]any {
	return map[string]any{
		"event_type":                   "lucky_gift_drawn",
		"event_id":                     "lucky_gift_big_win:" + payload.DrawID,
		"broadcast_type":               broadcastdomain.TypeLuckyGiftBigWin,
		"scope":                        broadcastdomain.ScopeRegion,
		"app_code":                     payload.AppCode,
		"region_id":                    payload.VisibleRegionID,
		"visible_region_id":            payload.VisibleRegionID,
		"draw_id":                      payload.DrawID,
		"command_id":                   payload.CommandID,
		"pool_id":                      payload.PoolID,
		"room_id":                      payload.RoomID,
		"gift_id":                      payload.GiftID,
		"gift_count":                   payload.GiftCount,
		"user_id":                      payload.UserID,
		"sender_user_id":               payload.SenderUserID,
		"target_user_id":               payload.TargetUserID,
		"coin_spent":                   payload.CoinSpent,
		"rule_version":                 payload.RuleVersion,
		"experience_pool":              payload.ExperiencePool,
		"selected_tier_id":             payload.SelectedTierID,
		"multiplier_ppm":               payload.MultiplierPPM,
		"base_reward_coins":            payload.BaseRewardCoins,
		"room_atmosphere_reward_coins": payload.RoomAtmosphereRewardCoins,
		"activity_subsidy_coins":       payload.ActivitySubsidyCoins,
		"effective_reward_coins":       payload.EffectiveRewardCoins,
		"stage_feedback":               payload.StageFeedback,
		"high_multiplier":              payload.HighMultiplier,
		"created_at_ms":                payload.CreatedAtMS,
		"sent_at_ms":                   sentAtMS,
		"action": map[string]any{
			"type":    "enter_room",
			"room_id": payload.RoomID,
		},
	}
}

func decodeDrawOutboxPayload(payloadJSON string) (luckyGiftDrawnPayload, error) {
	var payload luckyGiftDrawnPayload
	if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
		return luckyGiftDrawnPayload{}, fmt.Errorf("invalid lucky gift outbox payload: %w", err)
	}
	// 兼容早期 payload 缺省字段，避免历史 pending outbox 因新增字段永久失败。
	if payload.EventType == "" {
		payload.EventType = "lucky_gift_drawn"
	}
	// 兼容两类 outbox：老单抽 payload 只有 draw_id，新批量 payload 同时有聚合 draw_id 和 draw_ids。
	if payload.DrawID == "" && len(payload.DrawIDs) > 0 {
		payload.DrawID = payload.DrawIDs[0]
	}
	if len(payload.DrawIDs) == 0 && payload.DrawID != "" {
		payload.DrawIDs = []string{payload.DrawID}
	}
	if payload.EventID == "" && payload.DrawID != "" {
		payload.EventID = "lucky_gift_drawn:" + payload.DrawID
	}
	if payload.SenderUserID == 0 {
		payload.SenderUserID = payload.UserID
	}
	// 这些字段是返奖、房间 IM 和幂等去重的最小闭环，缺任何一个都不能安全补偿。
	if payload.DrawID == "" || payload.EventID == "" || payload.RoomID == "" || payload.GiftID == "" || payload.UserID <= 0 {
		return luckyGiftDrawnPayload{}, fmt.Errorf("lucky gift outbox payload is incomplete")
	}
	return payload, nil
}

// drawIDsFromPayload 只用于失败收敛；解析失败时返回 nil，让仓储层保持 no-op，避免误更新未知 draw。
func drawIDsFromPayload(payloadJSON string) []string {
	payload, err := decodeDrawOutboxPayload(payloadJSON)
	if err != nil {
		return nil
	}
	return payload.DrawIDs
}

func luckyDrawResultIDs(result domain.DrawResult) []string {
	if len(result.DrawIDs) > 0 {
		return append([]string(nil), result.DrawIDs...)
	}
	if strings.TrimSpace(result.DrawID) == "" {
		return nil
	}
	return []string{result.DrawID}
}

func normalizeWorkerOptions(options WorkerOptions) WorkerOptions {
	options.WorkerID = strings.TrimSpace(options.WorkerID)
	if options.WorkerID == "" {
		options.WorkerID = "lucky-gift-worker"
	}
	if options.PollInterval <= 0 {
		options.PollInterval = time.Second
	}
	if options.BatchSize <= 0 {
		options.BatchSize = 100
	}
	if options.BatchSize > 500 {
		options.BatchSize = 500
	}
	if options.Concurrency <= 0 {
		options.Concurrency = 4
	}
	if options.Concurrency > options.BatchSize {
		options.Concurrency = options.BatchSize
	}
	if options.LockTTL <= 0 {
		options.LockTTL = 30 * time.Second
	}
	if options.MaxRetry <= 0 {
		options.MaxRetry = 8
	}
	if options.PublishTimeout <= 0 {
		options.PublishTimeout = 5 * time.Second
	}
	if options.StatsInterval <= 0 {
		options.StatsInterval = 10 * time.Minute
	}
	if options.StatsBatchSize <= 0 {
		options.StatsBatchSize = 5000
	}
	if options.StatsBatchSize > 50000 {
		options.StatsBatchSize = 50000
	}
	return options
}

func luckyGiftBackoff(retryCount int) time.Duration {
	if retryCount < 1 {
		retryCount = 1
	}
	if retryCount > 6 {
		retryCount = 6
	}
	return time.Duration(1<<retryCount) * time.Second
}

func isRetryableLuckyGrantMarkError(err error) bool {
	if err == nil {
		return false
	}
	message := err.Error()
	return strings.Contains(message, "Deadlock") ||
		strings.Contains(message, "1213") ||
		strings.Contains(message, "40001") ||
		strings.Contains(message, "Lock wait timeout")
}
