package mysql

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

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

// GetCumulativeRechargeRewardConfig 读取累充奖励当前配置；未配置时后台可展示空草稿。
func (r *Repository) GetCumulativeRechargeRewardConfig(ctx context.Context) (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, cumulativeRechargeRewardConfigSelectSQL()+`
		WHERE app_code = ?`,
		appcode.FromContext(ctx),
	)
	config, err := scanCumulativeRechargeRewardConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Config{}, false, nil
	}
	if err != nil {
		return domain.Config{}, false, err
	}
	tiers, err := r.listCumulativeRechargeRewardTiers(ctx, nil)
	if err != nil {
		return domain.Config{}, false, err
	}
	config.Tiers = tiers
	return config, true, nil
}

// UpdateCumulativeRechargeRewardConfig 原子替换配置档位；已创建的 grant 保留命中时的档位快照。
func (r *Repository) UpdateCumulativeRechargeRewardConfig(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() }()

	_, err = tx.ExecContext(ctx, `
		INSERT INTO cumulative_recharge_reward_configs (
			app_code, 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.Enabled, config.UpdatedByAdminID, nowMS, nowMS,
	)
	if err != nil {
		return domain.Config{}, err
	}
	// 配置保存采用整组替换，避免后台删除/重排档位时保留脏行；历史 grant 已保存 tier 快照，不依赖当前档位表。
	if _, err := tx.ExecContext(ctx, `
		DELETE FROM cumulative_recharge_reward_tiers
		WHERE app_code = ?`,
		appcode.FromContext(ctx),
	); err != nil {
		return domain.Config{}, err
	}
	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 cumulative_recharge_reward_tiers (
					tier_id, app_code, tier_code, tier_name, threshold_usd_minor,
					resource_group_id, status, sort_order, created_at_ms, updated_at_ms
				) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
				tier.TierID, appcode.FromContext(ctx), tier.TierCode, tier.TierName, tier.ThresholdUSDMinor,
				tier.ResourceGroupID, tier.Status, tier.SortOrder, tier.CreatedAtMS, tier.UpdatedAtMS,
			); err != nil {
				return domain.Config{}, err
			}
			continue
		}
		result, err := tx.ExecContext(ctx, `
			INSERT INTO cumulative_recharge_reward_tiers (
				app_code, tier_code, tier_name, threshold_usd_minor,
				resource_group_id, status, sort_order, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			appcode.FromContext(ctx), tier.TierCode, tier.TierName, tier.ThresholdUSDMinor,
			tier.ResourceGroupID, 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.GetCumulativeRechargeRewardConfig(ctx)
	if err != nil {
		return domain.Config{}, err
	}
	if !exists {
		return domain.Config{}, xerr.New(xerr.NotFound, "cumulative recharge reward config not found")
	}
	return updated, nil
}

// GetCumulativeRechargeRewardProgress 读取用户当前周期累计；缺失代表本周还没有符合条件的充值。
func (r *Repository) GetCumulativeRechargeRewardProgress(ctx context.Context, cycleKey string, userID 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, cumulativeRechargeRewardProgressSelectSQL()+`
		WHERE app_code = ? AND cycle_key = ? AND user_id = ?`,
		appcode.FromContext(ctx), cycleKey, userID,
	)
	progress, err := scanCumulativeRechargeRewardProgress(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Progress{}, false, nil
	}
	return progress, err == nil, err
}

// ListCumulativeRechargeRewardGrantsByUserCycle 返回 H5 当前周期需要展示的用户发放状态。
func (r *Repository) ListCumulativeRechargeRewardGrantsByUserCycle(ctx context.Context, cycleKey string, userID int64) ([]domain.Grant, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	rows, err := r.db.QueryContext(ctx, cumulativeRechargeRewardGrantSelectSQL()+`
		WHERE app_code = ? AND cycle_key = ? AND user_id = ?
		ORDER BY threshold_usd_minor ASC, created_at_ms ASC, grant_id ASC`,
		appcode.FromContext(ctx), cycleKey, userID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	grants, _, err := scanCumulativeRechargeRewardGrants(rows, 0)
	return grants, err
}

// PrepareCumulativeRechargeRewardGrants 在 activity 事务内完成 event 幂等、周期累计和待发放 grant 创建。
func (r *Repository) PrepareCumulativeRechargeRewardGrants(ctx context.Context, event domain.RechargeEvent, cycle domain.Cycle, nowMS int64) (domain.PrepareResult, error) {
	if r == nil || r.db == nil {
		return domain.PrepareResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	defer func() { _ = tx.Rollback() }()

	// event_id 是 wallet_outbox 事实源幂等键；重复 MQ 消息不能再次增加 progress。
	inserted, err := r.insertCumulativeRechargeRewardEvent(ctx, tx, event, cycle.Key, nowMS)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	if !inserted {
		// 重复 event 只恢复该 event 上还没成功的 grant，不重新计算累计，也不根据新配置补发历史档位。
		grants, err := r.pendingCumulativeRechargeRewardGrantsByEventForUpdate(ctx, tx, event.EventID, nowMS)
		if err != nil {
			return domain.PrepareResult{}, err
		}
		if err := tx.Commit(); err != nil {
			return domain.PrepareResult{}, err
		}
		if len(grants) > 0 {
			return domain.PrepareResult{Grants: grants, Consumed: true, Reason: domain.ReasonPendingReward}, nil
		}
		return domain.PrepareResult{Consumed: true, Reason: domain.ReasonAlreadyConsumed}, nil
	}

	config, exists, err := r.getCumulativeRechargeRewardConfigForUpdate(ctx, tx)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	if !exists {
		if err := tx.Commit(); err != nil {
			return domain.PrepareResult{}, err
		}
		// 未配置时只记录 event 幂等事实，保证之后同一 event 不会因为补配置而被误当成新充值。
		return domain.PrepareResult{Consumed: true, Reason: domain.ReasonNotConfigured}, nil
	}
	if !config.Enabled {
		if err := tx.Commit(); err != nil {
			return domain.PrepareResult{}, err
		}
		// 禁用期间的充值不会进入累计；这是配置只影响后续事件的明确边界。
		return domain.PrepareResult{Consumed: true, Reason: domain.ReasonDisabled}, nil
	}
	tiers, err := r.listCumulativeRechargeRewardTiersForUpdate(ctx, tx)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	// 只有配置存在且启用后才累加 progress，避免后台开启活动时追溯禁用期事件。
	progress, err := r.upsertCumulativeRechargeRewardProgress(ctx, tx, event, cycle.Key, nowMS)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	// 当前充值可能一次跨过多个档位，所有命中的 active 档位都会在同一事务里创建 pending grant。
	grants, err := r.prepareMatchedCumulativeRechargeRewardGrants(ctx, tx, event, progress, tiers, nowMS)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	if err := tx.Commit(); err != nil {
		return domain.PrepareResult{}, err
	}
	if len(grants) == 0 {
		return domain.PrepareResult{Consumed: true, Reason: domain.ReasonNoTierMatched}, nil
	}
	return domain.PrepareResult{Grants: grants, Consumed: true, Reason: domain.ReasonEligible}, nil
}

// MarkCumulativeRechargeRewardGrantGranted 写回 wallet-service 资源组发放回执。
func (r *Repository) MarkCumulativeRechargeRewardGrantGranted(ctx context.Context, grantID string, walletGrantID string, grantedAtMS int64) (domain.Grant, error) {
	if r == nil || r.db == nil {
		return domain.Grant{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE cumulative_recharge_reward_grants
		SET status = 'granted',
		    wallet_grant_id = ?,
		    failure_reason = '',
		    granted_at_ms = ?,
		    updated_at_ms = ?
		WHERE app_code = ? AND grant_id = ? AND status <> 'granted'`,
		walletGrantID, grantedAtMS, grantedAtMS, appcode.FromContext(ctx), grantID,
	)
	if err != nil {
		return domain.Grant{}, err
	}
	grant, exists, err := r.getCumulativeRechargeRewardGrantByID(ctx, grantID)
	if err != nil {
		return domain.Grant{}, err
	}
	if !exists {
		return domain.Grant{}, xerr.New(xerr.NotFound, "cumulative recharge reward grant not found")
	}
	return grant, nil
}

// MarkCumulativeRechargeRewardGrantFailed 保留 grant，后续同一 wallet event 重试会复用原钱包命令。
func (r *Repository) MarkCumulativeRechargeRewardGrantFailed(ctx context.Context, grantID string, failureReason string, nowMS int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE cumulative_recharge_reward_grants
		SET status = 'failed', failure_reason = ?, updated_at_ms = ?
		WHERE app_code = ? AND grant_id = ? AND status <> 'granted'`,
		truncateTaskFailure(failureReason), nowMS, appcode.FromContext(ctx), grantID,
	)
	return err
}

// ListCumulativeRechargeRewardGrants 返回后台发放记录，按创建时间倒序。
func (r *Repository) ListCumulativeRechargeRewardGrants(ctx context.Context, query domain.GrantQuery) ([]domain.Grant, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	where, args := cumulativeRechargeRewardGrantWhere(ctx, query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM cumulative_recharge_reward_grants `+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	pageSize := normalizePageSize(query.PageSize)
	page := query.Page
	if page <= 0 {
		page = 1
	}
	args = append(args, pageSize, (page-1)*pageSize)
	rows, err := r.db.QueryContext(ctx, cumulativeRechargeRewardGrantSelectSQL()+`
		`+where+`
		ORDER BY created_at_ms DESC, grant_id DESC
		LIMIT ? OFFSET ?`, args...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	return scanCumulativeRechargeRewardGrants(rows, total)
}

func (r *Repository) insertCumulativeRechargeRewardEvent(ctx context.Context, tx *sql.Tx, event domain.RechargeEvent, cycleKey string, nowMS int64) (bool, error) {
	// INSERT IGNORE 让 wallet event 成为幂等边界：插入成功才允许进入累计和档位判定。
	result, err := tx.ExecContext(ctx, `
		INSERT IGNORE INTO cumulative_recharge_reward_events (
			app_code, event_id, cycle_key, user_id, transaction_id, command_id,
			recharge_type, recharge_coin_amount, qualifying_usd_minor, payload_json,
			occurred_at_ms, created_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx), event.EventID, cycleKey, event.UserID, event.TransactionID, event.CommandID,
		event.RechargeType, event.RechargeCoinAmount, event.QualifyingUSDMinor, event.PayloadJSON, event.OccurredAtMS, nowMS,
	)
	if err != nil {
		return false, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return false, err
	}
	return affected > 0, nil
}

func (r *Repository) upsertCumulativeRechargeRewardProgress(ctx context.Context, tx *sql.Tx, event domain.RechargeEvent, cycleKey string, nowMS int64) (domain.Progress, error) {
	// progress 是按 app + UTC 周 + user 聚合的金额投影；event 表先幂等后，这里可以安全做增量累加。
	_, err := tx.ExecContext(ctx, `
		INSERT INTO cumulative_recharge_reward_progress (
			app_code, cycle_key, user_id, total_usd_minor, total_coin_amount,
			first_recharged_at_ms, last_recharged_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			total_usd_minor = total_usd_minor + VALUES(total_usd_minor),
			total_coin_amount = total_coin_amount + VALUES(total_coin_amount),
			last_recharged_at_ms = GREATEST(last_recharged_at_ms, VALUES(last_recharged_at_ms)),
			updated_at_ms = VALUES(updated_at_ms)`,
		appcode.FromContext(ctx), cycleKey, event.UserID, event.QualifyingUSDMinor, event.RechargeCoinAmount,
		event.OccurredAtMS, event.OccurredAtMS, nowMS,
	)
	if err != nil {
		return domain.Progress{}, err
	}
	row := tx.QueryRowContext(ctx, cumulativeRechargeRewardProgressSelectSQL()+`
		WHERE app_code = ? AND cycle_key = ? AND user_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), cycleKey, event.UserID,
	)
	return scanCumulativeRechargeRewardProgress(row)
}

func (r *Repository) prepareMatchedCumulativeRechargeRewardGrants(ctx context.Context, tx *sql.Tx, event domain.RechargeEvent, progress domain.Progress, tiers []domain.Tier, nowMS int64) ([]domain.Grant, error) {
	prepared := make([]domain.Grant, 0)
	for _, tier := range tiers {
		// 只有 active 且资源组有效的档位可以发放；inactive 档位仍保留给后台展示和历史审计。
		if tier.Status != domain.TierStatusActive || tier.ResourceGroupID <= 0 || tier.ThresholdUSDMinor <= 0 {
			continue
		}
		if progress.TotalUSDMinor < tier.ThresholdUSDMinor {
			continue
		}
		// 唯一键 app/cycle/user/tier 保证每个 UTC 周每个档位最多创建一条 grant。
		existing, exists, err := r.getCumulativeRechargeRewardGrantByTierForUpdate(ctx, tx, progress.CycleKey, event.UserID, tier.TierID)
		if err != nil {
			return nil, err
		}
		if exists {
			if existing.Status == domain.GrantStatusFailed {
				// failed grant 代表 wallet 副作用没有确认成功，可以更新触发事件快照后再次尝试同一档位。
				if err := r.updateCumulativeRechargeRewardGrantPending(ctx, tx, existing.GrantID, event, progress.TotalUSDMinor, nowMS); err != nil {
					return nil, err
				}
				existing.Status = domain.GrantStatusPending
				existing.EventID = event.EventID
				existing.TransactionID = event.TransactionID
				existing.CommandID = event.CommandID
				existing.ReachedUSDMinor = progress.TotalUSDMinor
				existing.QualifyingUSDMinor = event.QualifyingUSDMinor
				existing.RechargeCoinAmount = event.RechargeCoinAmount
				existing.RechargeType = event.RechargeType
				existing.FailureReason = ""
				existing.UpdatedAtMS = nowMS
				prepared = append(prepared, existing)
			}
			if existing.Status == domain.GrantStatusPending {
				// pending grant 可能来自上次 MQ 消费已建单但还未完成 wallet 发放，直接返回给调用方继续执行。
				prepared = append(prepared, existing)
			}
			continue
		}
		grant := domain.Grant{
			GrantID:            idgen.New("crgrant"),
			AppCode:            appcode.FromContext(ctx),
			CycleKey:           progress.CycleKey,
			UserID:             event.UserID,
			EventID:            event.EventID,
			TransactionID:      event.TransactionID,
			CommandID:          event.CommandID,
			TierID:             tier.TierID,
			TierCode:           tier.TierCode,
			ThresholdUSDMinor:  tier.ThresholdUSDMinor,
			ResourceGroupID:    tier.ResourceGroupID,
			ReachedUSDMinor:    progress.TotalUSDMinor,
			QualifyingUSDMinor: event.QualifyingUSDMinor,
			RechargeCoinAmount: event.RechargeCoinAmount,
			RechargeType:       event.RechargeType,
			Status:             domain.GrantStatusPending,
			CreatedAtMS:        nowMS,
			UpdatedAtMS:        nowMS,
		}
		// wallet_command_id 从 grant_id 派生，数据库唯一键和 wallet 幂等命令共同防止重复到账。
		grant.WalletCommandID = walletCumulativeRechargeRewardCommandID(grant.GrantID)
		if err := r.insertCumulativeRechargeRewardGrant(ctx, tx, grant); err != nil {
			return nil, err
		}
		prepared = append(prepared, grant)
	}
	return prepared, nil
}

func (r *Repository) pendingCumulativeRechargeRewardGrantsByEventForUpdate(ctx context.Context, tx *sql.Tx, eventID string, nowMS int64) ([]domain.Grant, error) {
	// 重复 event 只查同一 event 创建过的 pending/failed grant，避免借 MQ 重投触发新配置下的额外发放。
	rows, err := tx.QueryContext(ctx, cumulativeRechargeRewardGrantSelectSQL()+`
		WHERE app_code = ? AND event_id = ? AND status IN ('pending', 'failed')
		ORDER BY threshold_usd_minor ASC, grant_id ASC
		FOR UPDATE`,
		appcode.FromContext(ctx), eventID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	grants, _, err := scanCumulativeRechargeRewardGrants(rows, 0)
	if err != nil {
		return nil, err
	}
	for index := range grants {
		if grants[index].Status != domain.GrantStatusFailed {
			continue
		}
		// failed 回到 pending 时不生成新 grant_id 或新 wallet_command_id，重试路径仍保持幂等。
		if err := r.updateCumulativeRechargeRewardGrantPendingByID(ctx, tx, grants[index].GrantID, nowMS); err != nil {
			return nil, err
		}
		grants[index].Status = domain.GrantStatusPending
		grants[index].FailureReason = ""
		grants[index].UpdatedAtMS = nowMS
	}
	return grants, nil
}

func (r *Repository) getCumulativeRechargeRewardConfigForUpdate(ctx context.Context, tx *sql.Tx) (domain.Config, bool, error) {
	row := tx.QueryRowContext(ctx, cumulativeRechargeRewardConfigSelectSQL()+`
		WHERE app_code = ?
		FOR UPDATE`,
		appcode.FromContext(ctx),
	)
	config, err := scanCumulativeRechargeRewardConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Config{}, false, nil
	}
	return config, err == nil, err
}

func (r *Repository) listCumulativeRechargeRewardTiers(ctx context.Context, tx *sql.Tx) ([]domain.Tier, error) {
	query := cumulativeRechargeRewardTierSelectSQL() + `
		WHERE app_code = ?
		ORDER BY sort_order DESC, threshold_usd_minor ASC, tier_id ASC`
	var rows *sql.Rows
	var err error
	if tx != nil {
		rows, err = tx.QueryContext(ctx, query, appcode.FromContext(ctx))
	} else {
		rows, err = r.db.QueryContext(ctx, query, appcode.FromContext(ctx))
	}
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCumulativeRechargeRewardTiers(rows)
}

func (r *Repository) listCumulativeRechargeRewardTiersForUpdate(ctx context.Context, tx *sql.Tx) ([]domain.Tier, error) {
	rows, err := tx.QueryContext(ctx, cumulativeRechargeRewardTierSelectSQL()+`
		WHERE app_code = ?
		ORDER BY sort_order DESC, threshold_usd_minor ASC, tier_id ASC
		FOR UPDATE`,
		appcode.FromContext(ctx),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCumulativeRechargeRewardTiers(rows)
}

func (r *Repository) getCumulativeRechargeRewardGrantByTierForUpdate(ctx context.Context, tx *sql.Tx, cycleKey string, userID int64, tierID int64) (domain.Grant, bool, error) {
	row := tx.QueryRowContext(ctx, cumulativeRechargeRewardGrantSelectSQL()+`
		WHERE app_code = ? AND cycle_key = ? AND user_id = ? AND tier_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), cycleKey, userID, tierID,
	)
	grant, err := scanCumulativeRechargeRewardGrant(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Grant{}, false, nil
	}
	return grant, err == nil, err
}

func (r *Repository) getCumulativeRechargeRewardGrantByID(ctx context.Context, grantID string) (domain.Grant, bool, error) {
	row := r.db.QueryRowContext(ctx, cumulativeRechargeRewardGrantSelectSQL()+`
		WHERE app_code = ? AND grant_id = ?`,
		appcode.FromContext(ctx), grantID,
	)
	grant, err := scanCumulativeRechargeRewardGrant(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Grant{}, false, nil
	}
	return grant, err == nil, err
}

func (r *Repository) insertCumulativeRechargeRewardGrant(ctx context.Context, tx *sql.Tx, grant domain.Grant) error {
	_, err := tx.ExecContext(ctx, `
		INSERT INTO cumulative_recharge_reward_grants (
			app_code, grant_id, cycle_key, user_id, event_id, transaction_id, command_id,
			tier_id, tier_code, threshold_usd_minor, resource_group_id, reached_usd_minor,
			qualifying_usd_minor, recharge_coin_amount, recharge_type, status, wallet_command_id,
			wallet_grant_id, failure_reason, granted_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', 0, ?, ?)`,
		appcode.FromContext(ctx), grant.GrantID, grant.CycleKey, grant.UserID, grant.EventID, grant.TransactionID, grant.CommandID,
		grant.TierID, grant.TierCode, grant.ThresholdUSDMinor, grant.ResourceGroupID, grant.ReachedUSDMinor,
		grant.QualifyingUSDMinor, grant.RechargeCoinAmount, grant.RechargeType, grant.Status, grant.WalletCommandID,
		grant.CreatedAtMS, grant.UpdatedAtMS,
	)
	return err
}

func (r *Repository) updateCumulativeRechargeRewardGrantPending(ctx context.Context, tx *sql.Tx, grantID string, event domain.RechargeEvent, reachedUSDMinor int64, nowMS int64) error {
	_, err := tx.ExecContext(ctx, `
		UPDATE cumulative_recharge_reward_grants
		SET event_id = ?,
		    transaction_id = ?,
		    command_id = ?,
		    reached_usd_minor = ?,
		    qualifying_usd_minor = ?,
		    recharge_coin_amount = ?,
		    recharge_type = ?,
		    status = 'pending',
		    wallet_grant_id = '',
		    failure_reason = '',
		    granted_at_ms = 0,
		    updated_at_ms = ?
		WHERE app_code = ? AND grant_id = ? AND status = 'failed'`,
		event.EventID, event.TransactionID, event.CommandID, reachedUSDMinor, event.QualifyingUSDMinor,
		event.RechargeCoinAmount, event.RechargeType, nowMS, appcode.FromContext(ctx), grantID,
	)
	return err
}

func (r *Repository) updateCumulativeRechargeRewardGrantPendingByID(ctx context.Context, tx *sql.Tx, grantID string, nowMS int64) error {
	_, err := tx.ExecContext(ctx, `
		UPDATE cumulative_recharge_reward_grants
		SET status = 'pending',
		    wallet_grant_id = '',
		    failure_reason = '',
		    granted_at_ms = 0,
		    updated_at_ms = ?
		WHERE app_code = ? AND grant_id = ? AND status = 'failed'`,
		nowMS, appcode.FromContext(ctx), grantID,
	)
	return err
}

func scanCumulativeRechargeRewardConfig(row rowScanner) (domain.Config, error) {
	var config domain.Config
	err := row.Scan(&config.AppCode, &config.Enabled, &config.UpdatedByAdminID, &config.CreatedAtMS, &config.UpdatedAtMS)
	return config, err
}

func scanCumulativeRechargeRewardTiers(rows *sql.Rows) ([]domain.Tier, error) {
	items := make([]domain.Tier, 0)
	for rows.Next() {
		var item domain.Tier
		if err := rows.Scan(
			&item.TierID,
			&item.TierCode,
			&item.TierName,
			&item.ThresholdUSDMinor,
			&item.ResourceGroupID,
			&item.Status,
			&item.SortOrder,
			&item.CreatedAtMS,
			&item.UpdatedAtMS,
		); err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func scanCumulativeRechargeRewardProgress(row rowScanner) (domain.Progress, error) {
	var item domain.Progress
	err := row.Scan(
		&item.AppCode,
		&item.CycleKey,
		&item.UserID,
		&item.TotalUSDMinor,
		&item.TotalCoinAmount,
		&item.FirstRechargedAtMS,
		&item.LastRechargedAtMS,
		&item.UpdatedAtMS,
	)
	return item, err
}

func scanCumulativeRechargeRewardGrants(rows *sql.Rows, total int64) ([]domain.Grant, int64, error) {
	items := make([]domain.Grant, 0)
	for rows.Next() {
		item, err := scanCumulativeRechargeRewardGrant(rows)
		if err != nil {
			return nil, 0, err
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, err
	}
	return items, total, nil
}

func scanCumulativeRechargeRewardGrant(row rowScanner) (domain.Grant, error) {
	var item domain.Grant
	err := row.Scan(
		&item.GrantID,
		&item.AppCode,
		&item.CycleKey,
		&item.UserID,
		&item.EventID,
		&item.TransactionID,
		&item.CommandID,
		&item.TierID,
		&item.TierCode,
		&item.ThresholdUSDMinor,
		&item.ResourceGroupID,
		&item.ReachedUSDMinor,
		&item.QualifyingUSDMinor,
		&item.RechargeCoinAmount,
		&item.RechargeType,
		&item.Status,
		&item.WalletCommandID,
		&item.WalletGrantID,
		&item.FailureReason,
		&item.GrantedAtMS,
		&item.CreatedAtMS,
		&item.UpdatedAtMS,
	)
	return item, err
}

func cumulativeRechargeRewardConfigSelectSQL() string {
	return `SELECT app_code, enabled, updated_by_admin_id, created_at_ms, updated_at_ms
	        FROM cumulative_recharge_reward_configs `
}

func cumulativeRechargeRewardTierSelectSQL() string {
	return `SELECT tier_id, tier_code, tier_name, threshold_usd_minor,
	               resource_group_id, status, sort_order, created_at_ms, updated_at_ms
	        FROM cumulative_recharge_reward_tiers `
}

func cumulativeRechargeRewardProgressSelectSQL() string {
	return `SELECT app_code, cycle_key, user_id, total_usd_minor, total_coin_amount,
	               first_recharged_at_ms, last_recharged_at_ms, updated_at_ms
	        FROM cumulative_recharge_reward_progress `
}

func cumulativeRechargeRewardGrantSelectSQL() string {
	return `SELECT grant_id, app_code, cycle_key, user_id, event_id, transaction_id, command_id,
	               tier_id, tier_code, threshold_usd_minor, resource_group_id, reached_usd_minor,
	               qualifying_usd_minor, recharge_coin_amount, recharge_type, status, wallet_command_id,
	               wallet_grant_id, failure_reason, granted_at_ms, created_at_ms, updated_at_ms
	        FROM cumulative_recharge_reward_grants `
}

func cumulativeRechargeRewardGrantWhere(ctx context.Context, query domain.GrantQuery) (string, []any) {
	conditions := []string{"app_code = ?"}
	args := []any{appcode.FromContext(ctx)}
	if query.Status != "" {
		conditions = append(conditions, "status = ?")
		args = append(args, query.Status)
	}
	if query.UserID > 0 {
		conditions = append(conditions, "user_id = ?")
		args = append(args, query.UserID)
	}
	if query.CycleKey != "" {
		conditions = append(conditions, "cycle_key = ?")
		args = append(args, query.CycleKey)
	}
	return "WHERE " + strings.Join(conditions, " AND "), args
}

func walletCumulativeRechargeRewardCommandID(grantID string) string {
	// 前缀区分累充奖励和其他 wallet 资源组发放来源，便于钱包侧排查命令来源。
	return fmt.Sprintf("wcr_%s", grantID)
}
