package mysql

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	lotteryengine "hyapp/services/lucky-gift-service/internal/domain/lotteryengine"
	domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)

type externalDrawRow struct {
	DrawID         string
	RequestID      string
	AppCode        string
	ExternalUserID string
	GiftCount      int64
	UnitAmount     int64
	TotalAmount    int64
	RewardAmount   int64
	MultiplierPPM  int64
	RewardStatus   string
	RuleVersion    int64
	CreatedAtMS    int64
}

func externalLuckyUserID(appCode string, externalUserID string) int64 {
	hash := sha256.Sum256([]byte(strings.Join([]string{appcode.Normalize(appCode), externalUserID}, "\x00")))
	const maxPositiveInt64 = ^uint64(0) >> 1
	// 外部 App 用户没有 HyApp user_id；稳定 hash 到正整数 ID，使用户阶段状态和抽奖审计可以复用统一字段。
	value := int64(binary.BigEndian.Uint64(hash[:8]) & maxPositiveInt64)
	if value == 0 {
		return 1
	}
	return value
}

func externalLuckyScopeID(appCode string) string {
	// 外部 App 接入没有房间维度；使用 app_code 的稳定 hash 作为虚拟房间作用域，避免污染真实房间奖池。
	return externalLuckyHashScope("external", appcode.Normalize(appCode))
}

func externalLuckyDeviceID(appCode string, deviceID string) string {
	// 外部设备 ID 最长可到 128 字符；hash 后写入内部审计/风控字段，
	// 既保持同 App+同设备的稳定 scope，也不把外部原始标识暴露到 draw 事实。
	return externalLuckyHashScope("external_device", appcode.Normalize(appCode), strings.TrimSpace(deviceID))
}

func externalLuckyFixedCompatibleDeviceID(cmd domain.ExternalDrawCommand) string {
	// fixed_v2 不解释新 device_id，始终保留历史 app+external_user_id 作用域。
	// 只有 dynamic_v3 才使用调用方绑定的设备，避免滚动升级改写 fixed 抽奖审计语义。
	return externalLuckyDeviceID(cmd.AppCode, cmd.ExternalUserID)
}

func externalLuckyAnchorID(appCode string) string {
	// 外部 App 没有主播维度；使用 app 级稳定 scope，避免和内部主播 ID 混用。
	return externalLuckyHashScope("external_anchor", appcode.Normalize(appCode))
}

func externalLuckyHashScope(prefix string, parts ...string) string {
	hash := sha256.Sum256([]byte(strings.Join(parts, "\x00")))
	return prefix + ":" + hex.EncodeToString(hash[:16])
}

// ExecuteExternalGiftDraw 在一个事务里完成外部 App 请求幂等、规则快照读取、随机抽奖、审计记录和 outbox。
// 调用方余额已经在自己的系统完成扣费，本事务不触碰 HyApp wallet；但 RTP、奖池和外部用户体验状态仍在本服务内推进。
func (r *Repository) ExecuteExternalGiftDraw(ctx context.Context, cmd domain.ExternalDrawCommand, nowMS int64) (domain.ExternalDrawResult, error) {
	if r == nil || r.db == nil {
		return domain.ExternalDrawResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.ExternalDrawResult{}, err
	}
	defer func() { _ = tx.Rollback() }()

	if err := r.lockExternalLuckyGiftRequest(ctx, tx, cmd.AppCode, cmd.RequestID, nowMS); err != nil {
		return domain.ExternalDrawResult{}, err
	}
	if existing, ok, err := r.getExternalGiftDrawForUpdate(ctx, tx, cmd.AppCode, cmd.RequestID); err != nil || ok {
		if err != nil {
			return domain.ExternalDrawResult{}, err
		}
		// 幂等命中直接返回原始结果，不能重新随机，也不能因为重试再次写 outbox。
		return externalDrawResultFromRow(existing), tx.Commit()
	}

	poolID := luckyPoolID(cmd.PoolID)
	// 外部用户与内部用户共用实验分流，只是 userKey 前缀不同；同一 external_user_id 的分组同样确定。
	// 与内部开奖一致必须先锁规则行（FOR UPDATE）：REPEATABLE READ 下无锁读会钉住
	// 上一个用户结算提交之前的快照，导致滚动 48 小时分子/分母漏掉已完成的开奖。
	rule, exists, err := r.resolveLuckyGiftRuleConfigForUser(ctx, tx, cmd.AppCode, poolID, domain.ExperimentExternalUserKey(cmd.ExternalUserID), true)
	if err != nil {
		return domain.ExternalDrawResult{}, err
	}
	if exists && rule.StrategyVersion == domain.StrategyDynamicV3 {
		if cmd.PaidAtMS <= 0 {
			// 外部 dynamic_v3 同样只能消费调用方已完成扣费的事实时间；请求到达 lucky 的时间
			// 不是支付时间，不能用于充值短窗、UTC 日/小时或精确滚动 48h 风控归属。
			return domain.ExternalDrawResult{}, xerr.New(xerr.InvalidArgument, "external dynamic lucky gift paid_at_ms is required")
		}
	} else if cmd.PaidAtMS <= 0 {
		// 未配置规则和历史 fixed_v2 保留滚动发布兼容；dynamic_v3 永远不会进入这条降级。
		cmd.PaidAtMS = nowMS
	}
	selected := externalDrawCandidate{TierID: "disabled", MultiplierPPM: 0}
	rewardAmount := int64(0)
	ruleVersion := rule.RuleVersion
	if exists && rule.Enabled {
		selected, rewardAmount, err = r.executeExternalGiftEconomy(ctx, tx, cmd, rule, nowMS)
		if err != nil {
			return domain.ExternalDrawResult{}, err
		}
	}
	drawID := idgen.New("external_lucky_draw")
	row := externalDrawRow{
		DrawID:         drawID,
		RequestID:      cmd.RequestID,
		AppCode:        cmd.AppCode,
		ExternalUserID: cmd.ExternalUserID,
		GiftCount:      cmd.GiftCount,
		UnitAmount:     cmd.UnitAmount,
		TotalAmount:    cmd.TotalAmount,
		RewardAmount:   rewardAmount,
		MultiplierPPM:  selected.MultiplierPPM,
		RewardStatus:   domain.StatusGranted,
		RuleVersion:    ruleVersion,
		CreatedAtMS:    nowMS,
	}
	metadata := strings.TrimSpace(cmd.MetadataJSON)
	if metadata == "" {
		metadata = "{}"
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO external_lucky_gift_draws (
		  app_code, request_id, draw_id, external_user_id, pool_id,
		  gift_count, unit_amount, total_amount, currency, metadata_json,
		  rule_version, selected_tier_id, multiplier_ppm, reward_amount, reward_status,
		  paid_at_ms, created_at_ms, updated_at_ms
		) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
		cmd.AppCode, cmd.RequestID, drawID, cmd.ExternalUserID, poolID,
		cmd.GiftCount, cmd.UnitAmount, cmd.TotalAmount, cmd.Currency, metadata,
		ruleVersion, selected.TierID, selected.MultiplierPPM, rewardAmount, row.RewardStatus,
		cmd.PaidAtMS, nowMS, nowMS,
	); err != nil {
		return domain.ExternalDrawResult{}, err
	}
	// dynamic_v3 已由逐抽 item 同事务写画像；fixed_v2 和停用规则没有 item，必须用聚合事实补一条，二者不能重复累计。
	if !(exists && rule.Enabled && rule.StrategyVersion == domain.StrategyDynamicV3) {
		strategyVersion := strings.TrimSpace(rule.StrategyVersion)
		if strategyVersion == "" {
			strategyVersion = domain.StrategyFixedV2
		}
		fact := luckyUserProfileFact{
			AppCode: cmd.AppCode, SourceType: luckyUserProfileSourceExternalLegacy, SourceDrawID: drawID,
			RequestID: cmd.RequestID, PoolID: poolID, IdentityType: domain.UserProfileIdentityExternal,
			IdentityKey: cmd.ExternalUserID, StrategyUserID: externalLuckyUserID(cmd.AppCode, cmd.ExternalUserID),
			ExternalUserID: cmd.ExternalUserID, RuleVersion: ruleVersion, StrategyVersion: strategyVersion,
			// fixed_v2 的外部请求把 gift_count 合并成一次随机：流水是整单 total_amount，
			// 但 paid_draws/loss_streak 只推进一次。画像必须沿用同一口径，不能把一条结果放大成 N 次命中。
			Stage: selected.Stage, DrawCount: 1, WagerCoins: cmd.TotalAmount,
			PayoutCoins: rewardAmount, BaseRewardCoins: rewardAmount, MultiplierPPM: selected.MultiplierPPM,
			RewardStatus: row.RewardStatus, TierID: selected.TierID, OccurredAtMS: cmd.PaidAtMS, CreatedAtMS: nowMS,
		}
		fact.HitType = luckyUserProfileHitType(fact.PayoutCoins, "", nil, nil)
		fact.HighOrdinary = selected.HighMultiplier && fact.HitType == domain.UserProfileHitOrdinary
		if err := r.persistLuckyGiftUserProfileFacts(ctx, tx, []luckyUserProfileFact{fact}, nowMS); err != nil {
			return domain.ExternalDrawResult{}, err
		}
	}
	payload, err := json.Marshal(map[string]any{
		"event_type":       domain.EventTypeExternalLuckyGiftDrawn,
		"event_id":         domain.EventTypeExternalLuckyGiftDrawn + ":" + drawID,
		"draw_id":          drawID,
		"app_code":         cmd.AppCode,
		"external_user_id": cmd.ExternalUserID,
		"request_id":       cmd.RequestID,
		"pool_id":          poolID,
		"gift_count":       cmd.GiftCount,
		"unit_amount":      cmd.UnitAmount,
		"total_amount":     cmd.TotalAmount,
		"reward_amount":    rewardAmount,
		"multiplier_ppm":   selected.MultiplierPPM,
		"reward_status":    row.RewardStatus,
		"rule_version":     ruleVersion,
		"created_at_ms":    nowMS,
	})
	if err != nil {
		return domain.ExternalDrawResult{}, err
	}
	if err := r.insertLuckyDrawOutbox(ctx, tx, cmd.AppCode, "external_lucky_gift_drawn:"+drawID, domain.EventTypeExternalLuckyGiftDrawn, payload, nowMS); err != nil {
		return domain.ExternalDrawResult{}, err
	}
	if err := tx.Commit(); err != nil {
		return domain.ExternalDrawResult{}, err
	}
	return externalDrawResultFromRow(row), nil
}

// lockExternalLuckyGiftRequest 为“尚不存在结果行”的并发幂等补上可锁主键。
// SELECT ... FOR UPDATE 无法锁住不存在的 external draw；先幂等建锁行后，第二个同 request_id 请求会等待首事务提交，再读回同一结果。
func (r *Repository) lockExternalLuckyGiftRequest(ctx context.Context, tx *sql.Tx, appCode, requestID string, nowMS int64) error {
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO external_lucky_gift_request_locks (app_code, request_id, created_at_ms)
		VALUES (?, ?, ?)
		ON DUPLICATE KEY UPDATE created_at_ms = created_at_ms`, appCode, requestID, nowMS,
	); err != nil {
		return err
	}
	var lockedRequestID string
	return tx.QueryRowContext(ctx, `
		SELECT request_id FROM external_lucky_gift_request_locks
		WHERE app_code = ? AND request_id = ? FOR UPDATE`, appCode, requestID,
	).Scan(&lockedRequestID)
}

func (r *Repository) executeExternalGiftEconomy(ctx context.Context, tx *sql.Tx, cmd domain.ExternalDrawCommand, rule domain.RuleConfig, nowMS int64) (externalDrawCandidate, int64, error) {
	if rule.StrategyVersion == domain.StrategyDynamicV3 {
		return r.executeExternalDynamicGiftEconomy(ctx, tx, cmd, rule, nowMS)
	}
	baseConfig, err := luckyRuntimeConfigFromRuleConfig(rule)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	referencePrice := baseConfig.GiftPrice
	config := luckyRuntimeConfig(baseConfig, cmd.TotalAmount)
	externalUserID := externalLuckyUserID(cmd.AppCode, cmd.ExternalUserID)
	roomScopeID := externalLuckyScopeID(cmd.AppCode)
	drawCommand := domain.DrawCommand{
		CommandID:       "external:" + cmd.AppCode + ":" + cmd.RequestID,
		PoolID:          cmd.PoolID,
		UserID:          externalUserID,
		DeviceID:        externalLuckyFixedCompatibleDeviceID(cmd),
		RoomID:          roomScopeID,
		AnchorID:        externalLuckyAnchorID(cmd.AppCode),
		GiftID:          config.GiftID,
		GiftCount:       int32(cmd.GiftCount),
		CoinSpent:       cmd.TotalAmount,
		PaidAtMS:        cmd.PaidAtMS,
		VisibleRegionID: 0,
		CountryID:       0,
	}
	userState, err := r.getLuckyUserStateForUpdate(ctx, tx, cmd.AppCode, externalUserID, config.GiftID, nowMS)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	nextCumulativeWager := userState.CumulativeWagerCoins + cmd.TotalAmount
	nextEquivalentDraws := luckyEquivalentDraws(nextCumulativeWager, referencePrice)
	experiencePool := luckyExperiencePool(nextEquivalentDraws, config)
	windowScopeID := luckyRuleWindowScopeID(config)
	globalWindow, err := r.getOpenLuckyRTPWindow(ctx, tx, cmd.AppCode, "pool", windowScopeID, config.GlobalWindowDraws, config.TargetRTPPPM, nowMS)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	globalWindow = globalWindow.withCurrentWager(cmd.TotalAmount)
	giftWindow, err := r.getOpenLuckyRTPWindow(ctx, tx, cmd.AppCode, "pool_gift", windowScopeID, config.GiftWindowDraws, config.TargetRTPPPM, nowMS)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	giftWindow = giftWindow.withCurrentWager(cmd.TotalAmount)
	// 外部 App 也使用 app+pool_id 单池；外部没有真实 HyApp 房间，因此不能再把 synthetic room 当资金隔离维度。
	basePool, err := r.getOrCreateLuckyPool(ctx, tx, cmd.AppCode, luckyBasePoolScopeType, luckyBasePoolScopeID(config), config.InitialBasePool, config.BasePoolReserve, nowMS)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	if err := r.creditLuckyBasePool(ctx, tx, cmd.AppCode, config, cmd.TotalAmount, &basePool, nowMS); err != nil {
		return externalDrawCandidate{}, 0, err
	}
	// 外部 fixed_v2 与内部路径共享同一口基础池，也必须走同一套高水位加权滞回，否则两侧 RTP 口径分叉。
	boostBefore := basePool.V2BoostActive
	if advanceLuckyV2HighWaterBoost(config, &basePool) {
		config = luckyV2BoostedConfig(config)
	}
	if basePool.V2BoostActive != boostBefore {
		if err := r.persistLuckyPoolV2BoostFlag(ctx, tx, cmd.AppCode, luckyBasePoolScopeType, basePool.ScopeID, basePool.V2BoostActive, nowMS); err != nil {
			return externalDrawCandidate{}, 0, err
		}
	}
	candidate, _, err := r.selectLuckyCandidate(config, experiencePool, basePool)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	if err := r.applyExternalLuckyEconomy(ctx, tx, cmd.AppCode, config, drawCommand, candidate, globalWindow, giftWindow, basePool, nextCumulativeWager, nextEquivalentDraws, nowMS); err != nil {
		return externalDrawCandidate{}, 0, err
	}
	return externalDrawCandidate{
		TierID: candidate.TierID, MultiplierPPM: candidate.MultiplierPPM,
		Stage: experiencePool, HighMultiplier: candidate.HighMultiplier,
	}, candidate.effectiveReward(), nil
}

// executeExternalDynamicGiftEconomy 让外部 App 复用与房间送礼完全相同的 dynamic_v3 内核。
// 外部调用方自己负责余额和主播收益结算，因此本服务按配置拆账、返回聚合 reward，且只写 external 子抽审计，绝不生成 HyApp wallet outbox。
func (r *Repository) executeExternalDynamicGiftEconomy(ctx context.Context, tx *sql.Tx, cmd domain.ExternalDrawCommand, rule domain.RuleConfig, nowMS int64) (externalDrawCandidate, int64, error) {
	trustedDeviceID, err := normalizeOptionalLuckyDynamicDeviceID(cmd.DeviceID)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	config, err := luckyRuntimeConfigFromRuleConfig(rule)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	strategyConfig := luckyDynamicStrategyConfig(config)
	split, err := domain.SplitLuckyGiftStrategyFunds(cmd.TotalAmount, strategyConfig)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	externalUserID := externalLuckyUserID(cmd.AppCode, cmd.ExternalUserID)
	dynamicDeviceID := ""
	if trustedDeviceID != "" {
		// 外部原始设备值只在调用方实际提供时生成内部稳定 scope；缺失时必须保持空值，
		// 否则所有无设备请求会错误共享一个“空设备”额度。
		dynamicDeviceID = externalLuckyDeviceID(cmd.AppCode, trustedDeviceID)
	}
	drawCommand := domain.DrawCommand{
		CommandID:          "external:" + cmd.AppCode + ":" + cmd.RequestID,
		PoolID:             config.PoolID,
		UserID:             externalUserID,
		DeviceID:           dynamicDeviceID,
		RoomID:             externalLuckyScopeID(cmd.AppCode),
		AnchorID:           externalLuckyAnchorID(cmd.AppCode),
		GiftID:             config.GiftID,
		GiftCount:          int32(cmd.GiftCount),
		CoinSpent:          cmd.TotalAmount,
		PaidAtMS:           cmd.PaidAtMS,
		UserRegisteredAtMS: cmd.UserRegisteredAtMS,
		// 外部 App 没有 HyApp wallet 回执，只能继续使用已签名外部金额按本规则拆出的主播份额。
		HostPeriodDiamondAdded: split.AnchorReturnCoins,
	}
	// 当前外部协议没有可验证的充值 owner 快照；保持 0/0 会落 novice 且关闭五分钟加成，
	// 比从 metadata 或客户端自报字段猜测更安全。后续若扩协议，必须把签名覆盖的充值事实显式入契约。
	ctx = appcode.WithContext(ctx, cmd.AppCode)
	results, err := r.executeDynamicLuckyGiftDrawBatch(ctx, tx, drawCommand, int32(cmd.GiftCount), rule, luckyDynamicPersistence{
		ExternalRequestID: cmd.RequestID,
		ExternalUserID:    cmd.ExternalUserID,
	}, nowMS)
	if err != nil {
		return externalDrawCandidate{}, 0, err
	}
	var reward int64
	for _, result := range results {
		if result.EffectiveRewardCoins < 0 || reward > int64(^uint64(0)>>1)-result.EffectiveRewardCoins {
			return externalDrawCandidate{}, 0, xerr.New(xerr.Internal, "external dynamic lucky gift reward overflow")
		}
		reward += result.EffectiveRewardCoins
	}
	candidate := externalDrawCandidate{TierID: "dynamic_v3_batch"}
	if len(results) > 0 {
		candidate.Stage = results[len(results)-1].ExperiencePool
	}
	if len(results) == 1 {
		candidate.TierID = results[0].SelectedTierID
		candidate.MultiplierPPM = results[0].MultiplierPPM
	} else if ratio, valid := (domain.StrategyRTP{WagerCoins: cmd.TotalAmount, PayoutCoins: reward}).RatioPPM(); valid {
		// 批量可能命中多个不同倍率；聚合 multiplier 仅表达 reward/total，逐抽真实倍率保存在 item 表。
		candidate.MultiplierPPM = ratio
	}
	return candidate, reward, nil
}

func (r *Repository) applyExternalLuckyEconomy(ctx context.Context, tx *sql.Tx, appCode string, config domain.Config, cmd domain.DrawCommand, candidate luckyCandidate, globalWindow luckyRTPWindow, giftWindow luckyRTPWindow, basePool luckyPool, nextCumulativeWager int64, nextEquivalentDraws int64, nowMS int64) error {
	if _, err := tx.ExecContext(ctx, `
		UPDATE lucky_rtp_windows
		SET paid_draws = paid_draws + 1,
		    wager_coins = ?,
		    target_payout_coins = ?,
		    actual_base_payout_coins = actual_base_payout_coins + ?,
		    carry_ppm = ?,
		    updated_at_ms = ?
		WHERE app_code = ? AND scope_type = ? AND scope_id = ? AND window_index = ?`,
		globalWindow.WagerCoins, globalWindow.TargetPayoutCoins, candidate.BaseReward, globalWindow.CarryPPM, nowMS,
		appCode, globalWindow.ScopeType, globalWindow.ScopeID, globalWindow.WindowIndex,
	); err != nil {
		return err
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE lucky_rtp_windows
		SET paid_draws = paid_draws + 1,
		    wager_coins = ?,
		    target_payout_coins = ?,
		    actual_base_payout_coins = actual_base_payout_coins + ?,
		    carry_ppm = ?,
		    updated_at_ms = ?
		WHERE app_code = ? AND scope_type = ? AND scope_id = ? AND window_index = ?`,
		giftWindow.WagerCoins, giftWindow.TargetPayoutCoins, candidate.BaseReward, giftWindow.CarryPPM, nowMS,
		appCode, giftWindow.ScopeType, giftWindow.ScopeID, giftWindow.WindowIndex,
	); err != nil {
		return err
	}
	// 外部抽奖只返回 reward_amount 给对方 App 结算，但 HyApp 内部仍要扣减单池，保证审计水位和内部路径一致。
	if err := r.debitLuckyPool(ctx, tx, appCode, luckyBasePoolScopeType, basePool.ScopeID, candidate.BaseReward, nowMS); err != nil {
		return err
	}
	lossStreakSQL := "loss_streak + 1"
	if candidate.effectiveReward() > 0 {
		lossStreakSQL = "0"
	}
	_, err := tx.ExecContext(ctx, `
		UPDATE lucky_user_states
		SET paid_draws = paid_draws + 1,
		    cumulative_wager_coins = ?,
		    equivalent_draws = ?,
		    loss_streak = `+lossStreakSQL+`,
		    updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND gift_id = ?`,
		nextCumulativeWager, nextEquivalentDraws, nowMS, appCode, cmd.UserID, config.GiftID,
	)
	return err
}

func (r *Repository) getExternalGiftDrawForUpdate(ctx context.Context, tx *sql.Tx, appCode string, requestID string) (externalDrawRow, bool, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT draw_id, request_id, app_code, external_user_id,
		       gift_count, unit_amount, total_amount, reward_amount, multiplier_ppm,
		       reward_status, rule_version, created_at_ms
		FROM external_lucky_gift_draws
		WHERE app_code = ? AND request_id = ?
		FOR UPDATE`, appCode, requestID)
	var item externalDrawRow
	if err := row.Scan(
		&item.DrawID, &item.RequestID, &item.AppCode, &item.ExternalUserID,
		&item.GiftCount, &item.UnitAmount, &item.TotalAmount, &item.RewardAmount, &item.MultiplierPPM,
		&item.RewardStatus, &item.RuleVersion, &item.CreatedAtMS,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return externalDrawRow{}, false, nil
		}
		return externalDrawRow{}, false, err
	}
	return item, true, nil
}

type externalDrawCandidate struct {
	TierID         string
	MultiplierPPM  int64
	Stage          string
	HighMultiplier bool
}

func selectExternalDrawCandidate(rule domain.RuleConfig) (externalDrawCandidate, error) {
	candidates := make([]lotteryengine.Candidate, 0, 8)
	for _, stage := range rule.Stages {
		if stage.Stage != domain.StageNormal && len(candidates) > 0 {
			continue
		}
		for _, tier := range stage.Tiers {
			if !tier.Enabled || tier.BaseWeightPPM <= 0 {
				continue
			}
			candidates = append(candidates, lotteryengine.Candidate{
				ID:            tier.TierID,
				Weight:        tier.BaseWeightPPM,
				PrizeKind:     lotteryengine.PrizeKindCoin,
				RTPValueCoins: tier.MultiplierPPM,
			})
		}
		if stage.Stage == domain.StageNormal && len(candidates) > 0 {
			break
		}
	}
	if len(candidates) == 0 {
		return externalDrawCandidate{TierID: "zero", MultiplierPPM: 0}, nil
	}
	selected, err := lotteryengine.SelectWeighted(candidates)
	if err != nil {
		return externalDrawCandidate{}, err
	}
	return externalDrawCandidate{TierID: selected.ID, MultiplierPPM: selected.RTPValueCoins}, nil
}

func externalDrawResultFromRow(row externalDrawRow) domain.ExternalDrawResult {
	return domain.ExternalDrawResult{
		DrawID:         row.DrawID,
		RequestID:      row.RequestID,
		AppCode:        row.AppCode,
		ExternalUserID: row.ExternalUserID,
		GiftCount:      row.GiftCount,
		UnitAmount:     row.UnitAmount,
		TotalAmount:    row.TotalAmount,
		RewardAmount:   row.RewardAmount,
		MultiplierPPM:  row.MultiplierPPM,
		RewardStatus:   row.RewardStatus,
		RuleVersion:    row.RuleVersion,
		CreatedAtMS:    row.CreatedAtMS,
	}
}

func (r *Repository) listExternalLuckyGiftDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error) {
	whereSQL, args := externalDrawWhereClause(query.AppCode, query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM external_lucky_gift_draws WHERE `+whereSQL, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	limit := int(query.PageSize)
	offset := int((query.Page - 1) * query.PageSize)
	args = append(args, limit, offset)
	rows, err := r.db.QueryContext(ctx, `
		SELECT draw_id, request_id, app_code, external_user_id, pool_id, rule_version, selected_tier_id,
		       multiplier_ppm, reward_amount, total_amount, reward_status, created_at_ms
		FROM external_lucky_gift_draws
		WHERE `+whereSQL+`
		ORDER BY created_at_ms DESC, draw_id DESC
		LIMIT ? OFFSET ?`, args...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	items := make([]domain.DrawResult, 0, limit)
	for rows.Next() {
		var item domain.DrawResult
		if err := rows.Scan(
			&item.DrawID, &item.CommandID, &item.AppCode, &item.ExternalUserID, &item.PoolID, &item.RuleVersion, &item.SelectedTierID,
			&item.MultiplierPPM, &item.EffectiveRewardCoins, &item.BaseRewardCoins, &item.RewardStatus, &item.CreatedAtMS,
		); err != nil {
			return nil, 0, err
		}
		// 外部 App 抽奖没有内部 gift_id/room 维度；request_id 放到 command_id，外部用户字段单独返回给中台展示和排查。
		item.DrawIDs = []string{item.DrawID}
		item.GiftID = "external"
		item.ExperiencePool = "external"
		items = append(items, item)
	}
	return items, total, rows.Err()
}

func (r *Repository) getExternalLuckyGiftDrawSummary(ctx context.Context, query domain.DrawQuery) (domain.DrawSummary, error) {
	whereSQL, args := externalDrawWhereClause(query.AppCode, query)
	var summary domain.DrawSummary
	summary.PoolID = query.PoolID
	if err := r.db.QueryRowContext(ctx, `
		SELECT
		  COUNT(*),
		  COUNT(DISTINCT external_user_id),
		  COALESCE(SUM(total_amount), 0),
		  COALESCE(SUM(reward_amount), 0),
		  COALESCE(SUM(CASE WHEN reward_status = 'granted' THEN 1 ELSE 0 END), 0),
		  COALESCE(SUM(CASE WHEN reward_status = 'failed' THEN 1 ELSE 0 END), 0)
		FROM external_lucky_gift_draws
		WHERE `+whereSQL, args...).Scan(
		&summary.TotalDraws,
		&summary.UniqueUsers,
		&summary.TotalSpentCoins,
		&summary.TotalRewardCoins,
		&summary.GrantedDraws,
		&summary.FailedDraws,
	); err != nil {
		return domain.DrawSummary{}, err
	}
	summary.BaseRewardCoins = summary.TotalRewardCoins
	if summary.TotalSpentCoins > 0 {
		summary.ActualRTPPPM = summary.TotalRewardCoins * luckyPPMScale / summary.TotalSpentCoins
	}
	return summary, nil
}

func externalDrawWhereClause(appCode string, query domain.DrawQuery) (string, []any) {
	where := []string{"1=1"}
	args := []any{}
	if strings.TrimSpace(appCode) != "" {
		where = append(where, "app_code = ?")
		args = append(args, appcode.Normalize(appCode))
	}
	if strings.TrimSpace(query.PoolID) != "" {
		where = append(where, "pool_id = ?")
		args = append(args, query.PoolID)
	}
	if strings.TrimSpace(query.ExternalUserID) != "" {
		where = append(where, "external_user_id = ?")
		args = append(args, query.ExternalUserID)
	}
	if strings.TrimSpace(query.Status) != "" {
		where = append(where, "reward_status = ?")
		args = append(args, query.Status)
	}
	return strings.Join(where, " AND "), args
}
