package mysql

import (
	"context"
	"database/sql"
	"errors"
	"strconv"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	domain "hyapp/services/activity-service/internal/domain/roomturnoverreward"
)

// GetRoomTurnoverRewardConfig 读取房间流水奖励当前配置；未配置是后台首屏正常状态。
func (r *Repository) GetRoomTurnoverRewardConfig(ctx context.Context, regionID int64) (domain.Config, bool, error) {
	if r == nil || r.db == nil {
		return domain.Config{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	row := r.db.QueryRowContext(ctx, roomTurnoverRewardConfigSelectSQL()+`
		WHERE app_code = ? AND region_id = ?`,
		appcode.FromContext(ctx), regionID,
	)
	config, err := scanRoomTurnoverRewardConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Config{}, false, nil
	}
	if err != nil {
		return domain.Config{}, false, err
	}
	tiers, err := r.listRoomTurnoverRewardTiers(ctx, nil, regionID)
	if err != nil {
		return domain.Config{}, false, err
	}
	config.Tiers = tiers
	return config, true, nil
}

// UpdateRoomTurnoverRewardConfig 原子替换配置档位；已生成的 settlement 保留命中档位快照。
func (r *Repository) UpdateRoomTurnoverRewardConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error) {
	if r == nil || r.db == nil {
		return domain.Config{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.Config{}, err
	}
	defer func() { _ = tx.Rollback() }()

	if _, err := tx.ExecContext(ctx, `
		INSERT INTO room_turnover_reward_configs (
			app_code, region_id, enabled, updated_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			enabled = VALUES(enabled),
			updated_by_admin_id = VALUES(updated_by_admin_id),
			updated_at_ms = VALUES(updated_at_ms)`,
		appcode.FromContext(ctx), config.RegionID, config.Enabled, config.UpdatedByAdminID, nowMS, nowMS,
	); err != nil {
		return domain.Config{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		DELETE FROM room_turnover_reward_tiers
		WHERE app_code = ? AND region_id = ?`,
		appcode.FromContext(ctx), config.RegionID,
	); err != nil {
		return domain.Config{}, err
	}
	// 配置保存采用“配置行 upsert + 档位整组替换”，让后台表单提交成为完整快照，避免删除档位后旧行继续参与后续匹配。
	for i := range config.Tiers {
		tier := config.Tiers[i]
		tier.CreatedAtMS = nowMS
		tier.UpdatedAtMS = nowMS
		if tier.TierID > 0 {
			if _, err := tx.ExecContext(ctx, `
				INSERT INTO room_turnover_reward_tiers (
					tier_id, app_code, region_id, tier_code, tier_name, threshold_coin_spent,
					reward_coin_amount, status, sort_order, created_at_ms, updated_at_ms
				) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
				tier.TierID, appcode.FromContext(ctx), config.RegionID, tier.TierCode, tier.TierName, tier.ThresholdCoinSpent,
				tier.RewardCoinAmount, tier.Status, tier.SortOrder, tier.CreatedAtMS, tier.UpdatedAtMS,
			); err != nil {
				return domain.Config{}, err
			}
			continue
		}
		result, err := tx.ExecContext(ctx, `
			INSERT INTO room_turnover_reward_tiers (
				app_code, region_id, tier_code, tier_name, threshold_coin_spent,
				reward_coin_amount, status, sort_order, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			appcode.FromContext(ctx), config.RegionID, tier.TierCode, tier.TierName, tier.ThresholdCoinSpent,
			tier.RewardCoinAmount, tier.Status, tier.SortOrder, tier.CreatedAtMS, tier.UpdatedAtMS,
		)
		if err != nil {
			return domain.Config{}, err
		}
		tierID, err := result.LastInsertId()
		if err != nil {
			return domain.Config{}, err
		}
		config.Tiers[i].TierID = tierID
	}
	if err := tx.Commit(); err != nil {
		return domain.Config{}, err
	}
	updated, exists, err := r.GetRoomTurnoverRewardConfig(ctx, config.RegionID)
	if err != nil {
		return domain.Config{}, err
	}
	if !exists {
		return domain.Config{}, xerr.New(xerr.NotFound, "room turnover reward config not found")
	}
	return updated, nil
}

// ConsumeRoomTurnoverGiftEvent 幂等消费 RoomGiftSent，并累加到房间所在 UTC 周期流水。
func (r *Repository) ConsumeRoomTurnoverGiftEvent(ctx context.Context, event domain.RoomGiftEvent, periodStartMS int64, periodEndMS int64, nowMS int64) (domain.EventResult, error) {
	if r == nil || r.db == nil {
		return domain.EventResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.EventResult{}, err
	}
	defer func() { _ = tx.Rollback() }()

	result, err := tx.ExecContext(ctx, `
		INSERT IGNORE INTO room_turnover_reward_event_consumption (
			app_code, event_id, event_type, room_id, period_start_ms, coin_spent,
			status, consumed_at_ms, created_at_ms, updated_at_ms, region_id
		) VALUES (?, ?, ?, ?, ?, ?, 'consumed', ?, ?, ?, ?)`,
		appcode.FromContext(ctx), event.EventID, event.EventType, event.RoomID, periodStartMS, event.CoinSpent, nowMS, nowMS, nowMS, event.RegionID,
	)
	if err != nil {
		return domain.EventResult{}, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return domain.EventResult{}, err
	}
	if affected == 0 {
		// event_id 是 room outbox 事实幂等键；重复投递只提交幂等事务，不再累加 progress，保证贡献值不会翻倍。
		if err := tx.Commit(); err != nil {
			return domain.EventResult{}, err
		}
		return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusDuplicate}, nil
	}
	// progress 是 App H5 和结算任务共用的读模型；同一事务先落消费幂等，再按 UTC 周期原子累加房间流水。
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO room_turnover_reward_progress (
			app_code, region_id, room_id, period_start_ms, period_end_ms, coin_spent,
			last_event_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			coin_spent = coin_spent + VALUES(coin_spent),
			period_end_ms = VALUES(period_end_ms),
			last_event_at_ms = GREATEST(last_event_at_ms, VALUES(last_event_at_ms)),
			updated_at_ms = VALUES(updated_at_ms)`,
		appcode.FromContext(ctx), event.RegionID, event.RoomID, periodStartMS, periodEndMS, event.CoinSpent, event.OccurredAtMS, nowMS, nowMS,
	); err != nil {
		return domain.EventResult{}, err
	}
	if err := tx.Commit(); err != nil {
		return domain.EventResult{}, err
	}
	return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusConsumed}, nil
}

// GetRoomTurnoverRewardProgress 返回指定房间和 UTC 周期的当前流水。
func (r *Repository) GetRoomTurnoverRewardProgress(ctx context.Context, regionID int64, roomID string, periodStartMS int64) (domain.Progress, bool, error) {
	if r == nil || r.db == nil {
		return domain.Progress{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	row := r.db.QueryRowContext(ctx, roomTurnoverRewardProgressSelectSQL()+`
		WHERE app_code = ? AND region_id = ? AND room_id = ? AND period_start_ms = ?`,
		appcode.FromContext(ctx), regionID, strings.TrimSpace(roomID), periodStartMS,
	)
	item, err := scanRoomTurnoverRewardProgress(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Progress{}, false, nil
	}
	return item, err == nil, err
}

func (r *Repository) GetLatestRoomTurnoverRewardSettlement(ctx context.Context, regionID int64, roomID string) (domain.Settlement, bool, error) {
	if r == nil || r.db == nil {
		return domain.Settlement{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	// 最新结算给 H5 展示上一周期结果；按 period_start_ms 倒序而不是 status 排序，保证失败和成功都能被用户看到。
	row := r.db.QueryRowContext(ctx, roomTurnoverRewardSettlementSelectSQL()+`
		WHERE app_code = ? AND region_id = ? AND room_id = ?
		ORDER BY period_start_ms DESC, created_at_ms DESC
		LIMIT 1`,
		appcode.FromContext(ctx), regionID, strings.TrimSpace(roomID),
	)
	item, err := scanRoomTurnoverRewardSettlement(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Settlement{}, false, nil
	}
	return item, err == nil, err
}

// ListRoomTurnoverRewardSettlements 返回后台结算记录页；查询只命中 settlement 快照，不回扫事件或 progress。
func (r *Repository) ListRoomTurnoverRewardSettlements(ctx context.Context, query domain.SettlementQuery) ([]domain.Settlement, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	// 后台列表只在 settlement 表上分页筛选；流水明细来自周期聚合快照，避免管理页回扫礼物事件。
	where := []string{"app_code = ?"}
	args := []any{appcode.FromContext(ctx)}
	if query.RegionID > 0 {
		where = append(where, "region_id = ?")
		args = append(args, query.RegionID)
	}
	if query.Status != "" {
		where = append(where, "status = ?")
		args = append(args, query.Status)
	}
	if strings.TrimSpace(query.RoomID) != "" {
		where = append(where, "room_id = ?")
		args = append(args, strings.TrimSpace(query.RoomID))
	}
	if query.OwnerUserID > 0 {
		where = append(where, "owner_user_id = ?")
		args = append(args, query.OwnerUserID)
	}
	if query.PeriodStartMS > 0 {
		where = append(where, "period_start_ms = ?")
		args = append(args, query.PeriodStartMS)
	}
	clause := "WHERE " + strings.Join(where, " AND ")
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM room_turnover_reward_settlements `+clause, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	pageSize := int(query.PageSize)
	if pageSize <= 0 {
		pageSize = 20
	}
	if pageSize > 100 {
		pageSize = 100
	}
	page := int(query.Page)
	if page <= 0 {
		page = 1
	}
	args = append(args, pageSize, (page-1)*pageSize)
	rows, err := r.db.QueryContext(ctx, roomTurnoverRewardSettlementSelectSQL()+clause+`
		ORDER BY period_start_ms DESC, created_at_ms DESC
		LIMIT ? OFFSET ?`, args...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	items, err := scanRoomTurnoverRewardSettlements(rows)
	return items, total, err
}

// ListUnsettledRoomTurnoverRewardProgress 找出上一周期有流水但还没有 settlement 的房间，用于 cron 补齐结算记录。
func (r *Repository) ListUnsettledRoomTurnoverRewardProgress(ctx context.Context, periodStartMS int64, limit int) ([]domain.Progress, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if limit <= 0 {
		limit = 100
	}
	// 具体区域配置存在时（即使关闭）优先于全区域配置；仅在区域配置不存在时回退 region_id=0，避免 disabled 区域被全局开关重新启用。
	rows, err := r.db.QueryContext(ctx, roomTurnoverRewardProgressSelectSQL()+`
		LEFT JOIN room_turnover_reward_configs regional_config
		  ON regional_config.app_code = progress.app_code
		 AND regional_config.region_id = progress.region_id
		LEFT JOIN room_turnover_reward_configs global_config
		  ON global_config.app_code = progress.app_code
		 AND global_config.region_id = 0
		LEFT JOIN room_turnover_reward_settlements settlement
		  ON settlement.app_code = progress.app_code
		 AND settlement.region_id = progress.region_id
		 AND settlement.room_id = progress.room_id
		 AND settlement.period_start_ms = progress.period_start_ms
		WHERE progress.app_code = ? AND progress.period_start_ms = ?
		  AND progress.coin_spent > 0 AND settlement.settlement_id IS NULL
		  AND COALESCE(regional_config.enabled, global_config.enabled, 0) = 1
		ORDER BY progress.coin_spent DESC, progress.room_id ASC
		LIMIT ?`,
		appcode.FromContext(ctx), periodStartMS, limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanRoomTurnoverRewardProgressRows(rows)
}

// InsertRoomTurnoverRewardSettlement 创建单房间单周期 settlement；已存在时读回原记录并返回 created=false。
func (r *Repository) InsertRoomTurnoverRewardSettlement(ctx context.Context, settlement domain.Settlement, nowMS int64) (domain.Settlement, bool, error) {
	if r == nil || r.db == nil {
		return domain.Settlement{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if settlement.SettlementID == "" {
		settlement.SettlementID = idgen.New("rtrsettle")
	}
	if settlement.WalletCommandID == "" {
		settlement.WalletCommandID = walletRoomTurnoverRewardCommandID(settlement.AppCode, settlement.RegionID, settlement.PeriodStartMS, settlement.RoomID)
	}
	settlement.CreatedAtMS = nowMS
	settlement.UpdatedAtMS = nowMS
	if settlement.Status == "" {
		settlement.Status = domain.SettlementStatusPending
	}
	// room_id + period_start_ms 是业务唯一键；cron 重跑、并发 worker 或人工补偿都只能为同一房间周期保留一条 settlement。
	result, err := r.db.ExecContext(ctx, `
		INSERT IGNORE INTO room_turnover_reward_settlements (
			app_code, region_id, settlement_id, room_id, owner_user_id, period_start_ms, period_end_ms,
			coin_spent, tier_id, tier_code, threshold_coin_spent, reward_coin_amount,
			status, wallet_command_id, wallet_transaction_id, failure_reason,
			settled_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, 0, ?, ?)`,
		appcode.FromContext(ctx), settlement.RegionID, settlement.SettlementID, settlement.RoomID, settlement.OwnerUserID,
		settlement.PeriodStartMS, settlement.PeriodEndMS, settlement.CoinSpent, settlement.TierID,
		settlement.TierCode, settlement.ThresholdCoinSpent, settlement.RewardCoinAmount,
		settlement.Status, settlement.WalletCommandID, settlement.FailureReason, nowMS, nowMS,
	)
	if err != nil {
		return domain.Settlement{}, false, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return domain.Settlement{}, false, err
	}
	item, exists, err := r.getRoomTurnoverRewardSettlementByRoomPeriod(ctx, settlement.RegionID, settlement.RoomID, settlement.PeriodStartMS)
	return item, affected > 0 && exists, err
}

// ListPendingRoomTurnoverRewardSettlements 返回等待发奖的 settlement；按老周期优先处理，避免历史失败恢复后长期排队。
func (r *Repository) ListPendingRoomTurnoverRewardSettlements(ctx context.Context, limit int) ([]domain.Settlement, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if limit <= 0 {
		limit = 100
	}
	rows, err := r.db.QueryContext(ctx, roomTurnoverRewardSettlementSelectSQL()+`
		WHERE app_code = ? AND status = ?
		ORDER BY period_start_ms ASC, created_at_ms ASC
		LIMIT ?`,
		appcode.FromContext(ctx), domain.SettlementStatusPending, limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanRoomTurnoverRewardSettlements(rows)
}

// GetRoomTurnoverRewardSettlement 按后台传入的 settlement_id 读取单条结算记录。
func (r *Repository) GetRoomTurnoverRewardSettlement(ctx context.Context, settlementID string) (domain.Settlement, bool, error) {
	if r == nil || r.db == nil {
		return domain.Settlement{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	row := r.db.QueryRowContext(ctx, roomTurnoverRewardSettlementSelectSQL()+`
		WHERE app_code = ? AND settlement_id = ?`,
		appcode.FromContext(ctx), strings.TrimSpace(settlementID),
	)
	item, err := scanRoomTurnoverRewardSettlement(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Settlement{}, false, nil
	}
	return item, err == nil, err
}

// ResetRoomTurnoverRewardSettlementForRetry 把失败记录重新放回 pending，并在 retry 时写入最新可用房主。
func (r *Repository) ResetRoomTurnoverRewardSettlementForRetry(ctx context.Context, settlementID string, ownerUserID int64, nowMS int64) (domain.Settlement, error) {
	if r == nil || r.db == nil {
		return domain.Settlement{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if _, err := r.db.ExecContext(ctx, `
		UPDATE room_turnover_reward_settlements
		SET status = ?, owner_user_id = ?, failure_reason = '', updated_at_ms = ?
		WHERE app_code = ? AND settlement_id = ? AND status <> ?`,
		domain.SettlementStatusPending, ownerUserID, nowMS, appcode.FromContext(ctx), strings.TrimSpace(settlementID), domain.SettlementStatusGranted,
	); err != nil {
		return domain.Settlement{}, err
	}
	// retry 只能把未发奖记录重新放回 pending；已经 granted 的记录不会被改回去，防止后台误点造成二次发奖。
	item, exists, err := r.GetRoomTurnoverRewardSettlement(ctx, settlementID)
	if err != nil {
		return domain.Settlement{}, err
	}
	if !exists {
		return domain.Settlement{}, xerr.New(xerr.NotFound, "room turnover reward settlement not found")
	}
	return item, nil
}

// MarkRoomTurnoverRewardSettlementGranted 记录钱包成功入账结果，是 activity 侧发奖状态的最终成功态。
func (r *Repository) MarkRoomTurnoverRewardSettlementGranted(ctx context.Context, settlementID string, walletTransactionID string, grantedAtMS int64) (domain.Settlement, error) {
	if r == nil || r.db == nil {
		return domain.Settlement{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	// 只有钱包 RPC 成功后才写 granted，并保存 wallet_transaction_id，后台和对账都以这条链路追踪真实入账。
	if _, err := r.db.ExecContext(ctx, `
		UPDATE room_turnover_reward_settlements
		SET status = ?, wallet_transaction_id = ?, failure_reason = '', settled_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND settlement_id = ?`,
		domain.SettlementStatusGranted, walletTransactionID, grantedAtMS, grantedAtMS, appcode.FromContext(ctx), strings.TrimSpace(settlementID),
	); err != nil {
		return domain.Settlement{}, err
	}
	item, exists, err := r.GetRoomTurnoverRewardSettlement(ctx, settlementID)
	if err != nil {
		return domain.Settlement{}, err
	}
	if !exists {
		return domain.Settlement{}, xerr.New(xerr.NotFound, "room turnover reward settlement not found")
	}
	return item, nil
}

// MarkRoomTurnoverRewardSettlementFailed 写入可展示、可 retry 的失败原因。
func (r *Repository) MarkRoomTurnoverRewardSettlementFailed(ctx context.Context, settlementID string, reason string, nowMS int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	// 失败标记不会覆盖 granted，避免钱包已成功但 activity-service 更新重试时被后续错误回写成失败。
	_, err := r.db.ExecContext(ctx, `
		UPDATE room_turnover_reward_settlements
		SET status = ?, failure_reason = ?, updated_at_ms = ?
		WHERE app_code = ? AND settlement_id = ? AND status <> ?`,
		domain.SettlementStatusFailed, trimFailureReason(reason), nowMS, appcode.FromContext(ctx), strings.TrimSpace(settlementID), domain.SettlementStatusGranted,
	)
	return err
}

func (r *Repository) getRoomTurnoverRewardSettlementByRoomPeriod(ctx context.Context, regionID int64, roomID string, periodStartMS int64) (domain.Settlement, bool, error) {
	row := r.db.QueryRowContext(ctx, roomTurnoverRewardSettlementSelectSQL()+`
		WHERE app_code = ? AND region_id = ? AND room_id = ? AND period_start_ms = ?`,
		appcode.FromContext(ctx), regionID, strings.TrimSpace(roomID), periodStartMS,
	)
	item, err := scanRoomTurnoverRewardSettlement(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Settlement{}, false, nil
	}
	return item, err == nil, err
}

func roomTurnoverRewardConfigSelectSQL() string {
	return `SELECT app_code, region_id, enabled, updated_by_admin_id, created_at_ms, updated_at_ms
		FROM room_turnover_reward_configs `
}

func roomTurnoverRewardTierSelectSQL() string {
	return `SELECT tier_id, app_code, region_id, tier_code, tier_name, threshold_coin_spent,
			reward_coin_amount, status, sort_order, created_at_ms, updated_at_ms
		FROM room_turnover_reward_tiers `
}

func roomTurnoverRewardProgressSelectSQL() string {
	return `SELECT progress.app_code, progress.region_id, progress.room_id, progress.period_start_ms, progress.period_end_ms,
			progress.coin_spent, progress.last_event_at_ms, progress.created_at_ms, progress.updated_at_ms
		FROM room_turnover_reward_progress progress `
}

func roomTurnoverRewardSettlementSelectSQL() string {
	return `SELECT app_code, region_id, settlement_id, room_id, owner_user_id, period_start_ms, period_end_ms,
			coin_spent, tier_id, tier_code, threshold_coin_spent, reward_coin_amount,
			status, wallet_command_id, wallet_transaction_id, failure_reason,
			settled_at_ms, created_at_ms, updated_at_ms
		FROM room_turnover_reward_settlements `
}

type roomTurnoverRewardConfigScanner interface {
	Scan(dest ...any) error
}

func scanRoomTurnoverRewardConfig(scanner roomTurnoverRewardConfigScanner) (domain.Config, error) {
	var item domain.Config
	err := scanner.Scan(&item.AppCode, &item.RegionID, &item.Enabled, &item.UpdatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS)
	return item, err
}

func (r *Repository) listRoomTurnoverRewardTiers(ctx context.Context, tx *sql.Tx, regionID int64) ([]domain.Tier, error) {
	query := roomTurnoverRewardTierSelectSQL() + `
		WHERE app_code = ? AND region_id = ?
		ORDER BY sort_order DESC, threshold_coin_spent ASC, tier_id ASC`
	var rows *sql.Rows
	var err error
	if tx != nil {
		rows, err = tx.QueryContext(ctx, query, appcode.FromContext(ctx), regionID)
	} else {
		rows, err = r.db.QueryContext(ctx, query, appcode.FromContext(ctx), regionID)
	}
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := make([]domain.Tier, 0)
	for rows.Next() {
		var item domain.Tier
		var app string
		var storedRegionID int64
		if err := rows.Scan(&item.TierID, &app, &storedRegionID, &item.TierCode, &item.TierName, &item.ThresholdCoinSpent, &item.RewardCoinAmount, &item.Status, &item.SortOrder, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

type roomTurnoverRewardProgressScanner interface {
	Scan(dest ...any) error
}

func scanRoomTurnoverRewardProgress(scanner roomTurnoverRewardProgressScanner) (domain.Progress, error) {
	var item domain.Progress
	err := scanner.Scan(&item.AppCode, &item.RegionID, &item.RoomID, &item.PeriodStartMS, &item.PeriodEndMS, &item.CoinSpent, &item.LastEventAtMS, &item.CreatedAtMS, &item.UpdatedAtMS)
	return item, err
}

func scanRoomTurnoverRewardProgressRows(rows *sql.Rows) ([]domain.Progress, error) {
	items := make([]domain.Progress, 0)
	for rows.Next() {
		item, err := scanRoomTurnoverRewardProgress(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

type roomTurnoverRewardSettlementScanner interface {
	Scan(dest ...any) error
}

func scanRoomTurnoverRewardSettlement(scanner roomTurnoverRewardSettlementScanner) (domain.Settlement, error) {
	var item domain.Settlement
	err := scanner.Scan(
		&item.AppCode, &item.RegionID, &item.SettlementID, &item.RoomID, &item.OwnerUserID,
		&item.PeriodStartMS, &item.PeriodEndMS, &item.CoinSpent,
		&item.TierID, &item.TierCode, &item.ThresholdCoinSpent, &item.RewardCoinAmount,
		&item.Status, &item.WalletCommandID, &item.WalletTransactionID, &item.FailureReason,
		&item.SettledAtMS, &item.CreatedAtMS, &item.UpdatedAtMS,
	)
	return item, err
}

func scanRoomTurnoverRewardSettlements(rows *sql.Rows) ([]domain.Settlement, error) {
	items := make([]domain.Settlement, 0)
	for rows.Next() {
		item, err := scanRoomTurnoverRewardSettlement(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func walletRoomTurnoverRewardCommandID(app string, regionID int64, periodStartMS int64, roomID string) string {
	// 区域属于结算业务键；同一房间在一周内迁区时，各区域贡献不能共享钱包幂等命令。
	return "room_turnover_reward:" + appcode.Normalize(app) + ":" + int64String(regionID) + ":" + int64String(periodStartMS) + ":" + strings.TrimSpace(roomID)
}

func int64String(value int64) string {
	return strconv.FormatInt(value, 10)
}

func trimFailureReason(reason string) string {
	reason = strings.TrimSpace(reason)
	if len(reason) > 512 {
		return reason[:512]
	}
	return reason
}
