package mysql

import (
	"context"
	"fmt"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
)

// DebitCPBreakupFee 在钱包账本中扣除解除关系费用；关系状态不在 wallet 中修改，避免 wallet 反向拥有用户关系事实。
func (r *Repository) DebitCPBreakupFee(ctx context.Context, command ledger.CPBreakupFeeCommand) (ledger.CPBreakupFeeReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.CPBreakupFeeReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.CPBreakupFeeReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := cpBreakupFeeRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeCPBreakupFee); err != nil || exists {
		if err != nil || !exists {
			return ledger.CPBreakupFeeReceipt{}, err
		}
		balance, balanceErr := r.balanceAfterTransaction(ctx, tx, txRow.TransactionID, command.UserID, ledger.AssetCoin)
		if balanceErr != nil {
			return ledger.CPBreakupFeeReceipt{}, balanceErr
		}
		return ledger.CPBreakupFeeReceipt{
			TransactionID:    txRow.TransactionID,
			CoinSpent:        command.Amount,
			CoinBalanceAfter: balance.AvailableAmount,
			Balance:          balance,
		}, nil
	}

	nowMs := time.Now().UnixMilli()
	account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs)
	if err != nil {
		return ledger.CPBreakupFeeReceipt{}, err
	}
	if account.AvailableAmount < command.Amount {
		return ledger.CPBreakupFeeReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount - command.Amount
	metadata := cpBreakupFeeMetadata{
		AppCode:          command.AppCode,
		UserID:           command.UserID,
		RelationshipID:   command.RelationshipID,
		RelationType:     command.RelationType,
		Amount:           command.Amount,
		CoinBalanceAfter: balanceAfter,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeCPBreakupFee, requestHash, command.RelationshipID, metadata, nowMs); err != nil {
		return ledger.CPBreakupFeeReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, -command.Amount, 0, nowMs); err != nil {
		return ledger.CPBreakupFeeReceipt{}, err
	}
	balance := ledger.AssetBalance{
		AppCode:         command.AppCode,
		UserID:          command.UserID,
		AssetType:       ledger.AssetCoin,
		AvailableAmount: balanceAfter,
		FrozenAmount:    account.FrozenAmount,
		Version:         account.Version + 1,
		UpdatedAtMs:     nowMs,
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: -command.Amount,
		FrozenDelta:    0,
		AvailableAfter: balance.AvailableAmount,
		FrozenAfter:    balance.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.CPBreakupFeeReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, -command.Amount, 0, balance.AvailableAmount, balance.FrozenAmount, balance.Version, metadata, nowMs, bizTypeCPBreakupFee),
	}); err != nil {
		return ledger.CPBreakupFeeReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.CPBreakupFeeReceipt{}, err
	}
	return ledger.CPBreakupFeeReceipt{
		TransactionID:    transactionID,
		CoinSpent:        command.Amount,
		CoinBalanceAfter: balance.AvailableAmount,
		Balance:          balance,
	}, nil
}

// DebitWheelDraw 在钱包账本中扣除转盘抽奖金币；中奖和返奖由 activity-service 后续处理，钱包只保证付费事实可审计、可幂等。
func (r *Repository) DebitWheelDraw(ctx context.Context, command ledger.WheelDrawDebitCommand) (ledger.WheelDrawDebitReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.WheelDrawDebitReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.WheelDrawDebitReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := wheelDrawDebitRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeWheelDrawDebit); err != nil || exists {
		if err != nil || !exists {
			return ledger.WheelDrawDebitReceipt{}, err
		}
		balance, balanceErr := r.balanceAfterTransaction(ctx, tx, txRow.TransactionID, command.UserID, ledger.AssetCoin)
		if balanceErr != nil {
			return ledger.WheelDrawDebitReceipt{}, balanceErr
		}
		return ledger.WheelDrawDebitReceipt{
			TransactionID:    txRow.TransactionID,
			CoinSpent:        command.Amount,
			CoinBalanceAfter: balance.AvailableAmount,
			Balance:          balance,
		}, nil
	}

	nowMs := time.Now().UnixMilli()
	account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs)
	if err != nil {
		return ledger.WheelDrawDebitReceipt{}, err
	}
	if account.AvailableAmount < command.Amount {
		return ledger.WheelDrawDebitReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount - command.Amount
	metadata := wheelDrawDebitMetadata{
		AppCode:          command.AppCode,
		UserID:           command.UserID,
		WheelID:          command.WheelID,
		DrawCount:        command.DrawCount,
		Amount:           command.Amount,
		CoinBalanceAfter: balanceAfter,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeWheelDrawDebit, requestHash, command.WheelID, metadata, nowMs); err != nil {
		return ledger.WheelDrawDebitReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, -command.Amount, 0, nowMs); err != nil {
		return ledger.WheelDrawDebitReceipt{}, err
	}
	balance := ledger.AssetBalance{
		AppCode:         command.AppCode,
		UserID:          command.UserID,
		AssetType:       ledger.AssetCoin,
		AvailableAmount: balanceAfter,
		FrozenAmount:    account.FrozenAmount,
		Version:         account.Version + 1,
		UpdatedAtMs:     nowMs,
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: -command.Amount,
		FrozenDelta:    0,
		AvailableAfter: balance.AvailableAmount,
		FrozenAfter:    balance.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.WheelDrawDebitReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, -command.Amount, 0, balance.AvailableAmount, balance.FrozenAmount, balance.Version, metadata, nowMs, bizTypeWheelDrawDebit),
	}); err != nil {
		return ledger.WheelDrawDebitReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.WheelDrawDebitReceipt{}, err
	}
	return ledger.WheelDrawDebitReceipt{
		TransactionID:    transactionID,
		CoinSpent:        command.Amount,
		CoinBalanceAfter: balance.AvailableAmount,
		Balance:          balance,
	}, nil
}

// CreditTaskReward 在一个事务内完成任务奖励资产入账、交易分录和 outbox 事件。
func (r *Repository) CreditTaskReward(ctx context.Context, command ledger.TaskRewardCommand) (ledger.TaskRewardReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.TaskRewardReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	command.AssetType = strings.ToUpper(strings.TrimSpace(command.AssetType))
	if command.AssetType == "" {
		command.AssetType = ledger.AssetCoin
	}

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.TaskRewardReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := taskRewardRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeTaskReward); err != nil || exists {
		if err != nil || !exists {
			return ledger.TaskRewardReceipt{}, err
		}
		return r.receiptForTaskRewardTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	account, err := r.lockAccount(ctx, tx, command.TargetUserID, command.AssetType, true, nowMs)
	if err != nil {
		return ledger.TaskRewardReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount + command.Amount
	metadata := taskRewardMetadata{
		AppCode:      command.AppCode,
		TargetUserID: command.TargetUserID,
		AssetType:    command.AssetType,
		Amount:       command.Amount,
		TaskType:     command.TaskType,
		TaskID:       command.TaskID,
		CycleKey:     command.CycleKey,
		Reason:       command.Reason,
		BalanceAfter: balanceAfter,
		GrantedAtMS:  nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeTaskReward, requestHash, fmt.Sprintf("%s:%s", command.TaskID, command.CycleKey), metadata, nowMs); err != nil {
		return ledger.TaskRewardReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil {
		return ledger.TaskRewardReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.TargetUserID,
		AssetType:      command.AssetType,
		AvailableDelta: command.Amount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.TaskRewardReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, command.AssetType, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs),
		taskRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs),
	}); err != nil {
		return ledger.TaskRewardReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.TaskRewardReceipt{}, err
	}

	return receiptFromTaskRewardMetadata(transactionID, metadata), nil
}

// CreditLuckyGiftReward 在同一事务里完成幸运礼物中奖 COIN 入账、交易分录和钱包 outbox。
func (r *Repository) CreditLuckyGiftReward(ctx context.Context, command ledger.LuckyGiftRewardCommand) (ledger.LuckyGiftRewardReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.LuckyGiftRewardReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.LuckyGiftRewardReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := luckyGiftRewardRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeLuckyGiftReward); err != nil || exists {
		if err != nil || !exists {
			return ledger.LuckyGiftRewardReceipt{}, err
		}
		return r.receiptForLuckyGiftRewardTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs)
	if err != nil {
		return ledger.LuckyGiftRewardReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount + command.Amount
	metadata := luckyGiftRewardMetadata{
		AppCode:         command.AppCode,
		TargetUserID:    command.TargetUserID,
		AssetType:       ledger.AssetCoin,
		Amount:          command.Amount,
		DrawID:          command.DrawID,
		RoomID:          command.RoomID,
		VisibleRegionID: command.VisibleRegionID,
		CountryID:       command.CountryID,
		GiftID:          command.GiftID,
		PoolID:          command.PoolID,
		Reason:          command.Reason,
		BalanceAfter:    balanceAfter,
		GrantedAtMS:     nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeLuckyGiftReward, requestHash, command.DrawID, metadata, nowMs); err != nil {
		return ledger.LuckyGiftRewardReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil {
		return ledger.LuckyGiftRewardReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: command.Amount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		RoomID:         command.RoomID,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.LuckyGiftRewardReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs),
		luckyGiftRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs),
	}); err != nil {
		return ledger.LuckyGiftRewardReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.LuckyGiftRewardReceipt{}, err
	}

	return receiptFromLuckyGiftRewardMetadata(transactionID, metadata), nil
}

// CreditWheelReward 在同一事务里完成转盘金币奖品入账、交易分录和钱包 outbox。
func (r *Repository) CreditWheelReward(ctx context.Context, command ledger.WheelRewardCommand) (ledger.WheelRewardReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.WheelRewardReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.WheelRewardReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := wheelRewardRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeWheelReward); err != nil || exists {
		if err != nil || !exists {
			return ledger.WheelRewardReceipt{}, err
		}
		return r.receiptForWheelRewardTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs)
	if err != nil {
		return ledger.WheelRewardReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount + command.Amount
	metadata := wheelRewardMetadata{
		AppCode:         command.AppCode,
		TargetUserID:    command.TargetUserID,
		AssetType:       ledger.AssetCoin,
		Amount:          command.Amount,
		DrawID:          command.DrawID,
		WheelID:         command.WheelID,
		SelectedTierID:  command.SelectedTierID,
		VisibleRegionID: command.VisibleRegionID,
		Reason:          command.Reason,
		BalanceAfter:    balanceAfter,
		GrantedAtMS:     nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeWheelReward, requestHash, command.DrawID, metadata, nowMs); err != nil {
		return ledger.WheelRewardReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil {
		return ledger.WheelRewardReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: command.Amount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.WheelRewardReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs),
		wheelRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs),
	}); err != nil {
		return ledger.WheelRewardReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.WheelRewardReceipt{}, err
	}

	return receiptFromWheelRewardMetadata(transactionID, metadata), nil
}

// CreditRoomTurnoverReward 在同一事务里完成每周房间流水奖励 COIN 入账、交易分录和钱包 outbox。
func (r *Repository) CreditRoomTurnoverReward(ctx context.Context, command ledger.RoomTurnoverRewardCommand) (ledger.RoomTurnoverRewardReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.RoomTurnoverRewardReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.RoomTurnoverRewardReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := roomTurnoverRewardRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeRoomTurnoverReward); err != nil || exists {
		if err != nil || !exists {
			return ledger.RoomTurnoverRewardReceipt{}, err
		}

		return r.receiptForRoomTurnoverRewardTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()

	account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs)
	if err != nil {
		return ledger.RoomTurnoverRewardReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount + command.Amount

	metadata := roomTurnoverRewardMetadata{
		AppCode:       command.AppCode,
		TargetUserID:  command.TargetUserID,
		AssetType:     ledger.AssetCoin,
		Amount:        command.Amount,
		SettlementID:  command.SettlementID,
		RoomID:        command.RoomID,
		PeriodStartMS: command.PeriodStartMS,
		PeriodEndMS:   command.PeriodEndMS,
		CoinSpent:     command.CoinSpent,
		TierID:        command.TierID,
		TierCode:      command.TierCode,
		Reason:        command.Reason,
		BalanceAfter:  balanceAfter,
		GrantedAtMS:   nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeRoomTurnoverReward, requestHash, command.SettlementID, metadata, nowMs); err != nil {
		return ledger.RoomTurnoverRewardReceipt{}, err
	}

	if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil {
		return ledger.RoomTurnoverRewardReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: command.Amount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		RoomID:         command.RoomID,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.RoomTurnoverRewardReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs),
		roomTurnoverRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs),
	}); err != nil {
		return ledger.RoomTurnoverRewardReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.RoomTurnoverRewardReceipt{}, err
	}

	return receiptFromRoomTurnoverRewardMetadata(transactionID, metadata), nil
}

// CreditInviteActivityReward 在同一事务内完成邀请活动 COIN 奖励入账、交易分录和钱包 outbox。
func (r *Repository) CreditInviteActivityReward(ctx context.Context, command ledger.InviteActivityRewardCommand) (ledger.InviteActivityRewardReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.InviteActivityRewardReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.InviteActivityRewardReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := inviteActivityRewardRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeInviteActivityReward); err != nil || exists {
		if err != nil || !exists {
			return ledger.InviteActivityRewardReceipt{}, err
		}

		return r.receiptForInviteActivityRewardTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs)
	if err != nil {
		return ledger.InviteActivityRewardReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount + command.Amount
	metadata := inviteActivityRewardMetadata{
		AppCode:      command.AppCode,
		TargetUserID: command.TargetUserID,
		AssetType:    ledger.AssetCoin,
		Amount:       command.Amount,
		ClaimID:      command.ClaimID,
		RewardType:   command.RewardType,
		TierID:       command.TierID,
		TierCode:     command.TierCode,
		CycleKey:     command.CycleKey,
		ReachedValue: command.ReachedValue,
		Reason:       command.Reason,
		BalanceAfter: balanceAfter,
		GrantedAtMS:  nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeInviteActivityReward, requestHash, command.ClaimID, metadata, nowMs); err != nil {
		return ledger.InviteActivityRewardReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil {
		return ledger.InviteActivityRewardReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: command.Amount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.InviteActivityRewardReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs),
		inviteActivityRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs),
	}); err != nil {
		return ledger.InviteActivityRewardReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.InviteActivityRewardReceipt{}, err
	}

	return receiptFromInviteActivityRewardMetadata(transactionID, metadata), nil
}

// CreditAgencyOpeningReward 在同一事务内完成代理开业流水档位 COIN 奖励入账、交易分录和钱包 outbox。
func (r *Repository) CreditAgencyOpeningReward(ctx context.Context, command ledger.AgencyOpeningRewardCommand) (ledger.AgencyOpeningRewardReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.AgencyOpeningRewardReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.AgencyOpeningRewardReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := agencyOpeningRewardRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeAgencyOpeningReward); err != nil || exists {
		if err != nil || !exists {
			return ledger.AgencyOpeningRewardReceipt{}, err
		}
		return r.receiptForAgencyOpeningRewardTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs)
	if err != nil {
		return ledger.AgencyOpeningRewardReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount + command.Amount
	metadata := agencyOpeningRewardMetadata{
		AppCode:       command.AppCode,
		TargetUserID:  command.TargetUserID,
		AssetType:     ledger.AssetCoin,
		Amount:        command.Amount,
		ApplicationID: command.ApplicationID,
		CycleID:       command.CycleID,
		AgencyID:      command.AgencyID,
		RankNo:        command.RankNo,
		ScoreCoins:    command.ScoreCoins,
		Reason:        command.Reason,
		BalanceAfter:  balanceAfter,
		GrantedAtMS:   nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeAgencyOpeningReward, requestHash, command.ApplicationID, metadata, nowMs); err != nil {
		return ledger.AgencyOpeningRewardReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil {
		return ledger.AgencyOpeningRewardReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: command.Amount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.AgencyOpeningRewardReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs),
		agencyOpeningRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs),
	}); err != nil {
		return ledger.AgencyOpeningRewardReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.AgencyOpeningRewardReceipt{}, err
	}

	return receiptFromAgencyOpeningRewardMetadata(transactionID, metadata), nil
}

func cpBreakupFeeRequestHash(command ledger.CPBreakupFeeCommand) string {

	return stableHash(fmt.Sprintf("cp_breakup_fee|%s|%d|%s|%s|%d",
		appcode.Normalize(command.AppCode),
		command.UserID,
		strings.TrimSpace(command.RelationshipID),
		strings.ToLower(strings.TrimSpace(command.RelationType)),
		command.Amount,
	))
}

func wheelDrawDebitRequestHash(command ledger.WheelDrawDebitCommand) string {

	return stableHash(fmt.Sprintf("wheel_draw_debit|%s|%d|%s|%d|%d",
		appcode.Normalize(command.AppCode),
		command.UserID,
		strings.TrimSpace(command.WheelID),
		command.DrawCount,
		command.Amount,
	))
}

func taskRewardRequestHash(command ledger.TaskRewardCommand) string {
	return stableHash(fmt.Sprintf("task_reward|%s|%d|%s|%d|%s|%s|%s|%s",
		appcode.Normalize(command.AppCode),
		command.TargetUserID,
		strings.ToUpper(strings.TrimSpace(command.AssetType)),
		command.Amount,
		command.TaskType,
		command.TaskID,
		command.CycleKey,
		strings.TrimSpace(command.Reason),
	))
}

func luckyGiftRewardRequestHash(command ledger.LuckyGiftRewardCommand) string {
	return stableHash(fmt.Sprintf("lucky_gift_reward|%s|%d|%d|%s|%s|%d|%d|%s|%s|%s",
		appcode.Normalize(command.AppCode),
		command.TargetUserID,
		command.Amount,
		strings.TrimSpace(command.DrawID),
		strings.TrimSpace(command.RoomID),
		command.VisibleRegionID,
		command.CountryID,
		strings.TrimSpace(command.GiftID),
		strings.TrimSpace(command.PoolID),
		strings.TrimSpace(command.Reason),
	))
}

func wheelRewardRequestHash(command ledger.WheelRewardCommand) string {
	return stableHash(fmt.Sprintf("wheel_reward|%s|%d|%d|%s|%s|%s|%d|%s",
		appcode.Normalize(command.AppCode),
		command.TargetUserID,
		command.Amount,
		strings.TrimSpace(command.DrawID),
		strings.TrimSpace(command.WheelID),
		strings.TrimSpace(command.SelectedTierID),
		command.VisibleRegionID,
		strings.TrimSpace(command.Reason),
	))
}

func roomTurnoverRewardRequestHash(command ledger.RoomTurnoverRewardCommand) string {
	return stableHash(fmt.Sprintf("room_turnover_reward|%s|%d|%d|%s|%s|%d|%d|%d|%d|%s|%s",
		appcode.Normalize(command.AppCode),
		command.TargetUserID,
		command.Amount,
		strings.TrimSpace(command.SettlementID),
		strings.TrimSpace(command.RoomID),
		command.PeriodStartMS,
		command.PeriodEndMS,
		command.CoinSpent,
		command.TierID,
		strings.TrimSpace(command.TierCode),
		strings.TrimSpace(command.Reason),
	))
}

func inviteActivityRewardRequestHash(command ledger.InviteActivityRewardCommand) string {
	return stableHash(fmt.Sprintf("invite_activity_reward|%s|%d|%d|%s|%s|%d|%s|%s|%d|%s",
		appcode.Normalize(command.AppCode),
		command.TargetUserID,
		command.Amount,
		strings.TrimSpace(command.ClaimID),
		strings.TrimSpace(command.RewardType),
		command.TierID,
		strings.TrimSpace(command.TierCode),
		strings.TrimSpace(command.CycleKey),
		command.ReachedValue,
		strings.TrimSpace(command.Reason),
	))
}

func agencyOpeningRewardRequestHash(command ledger.AgencyOpeningRewardCommand) string {
	return stableHash(fmt.Sprintf("agency_opening_reward|%s|%d|%d|%s|%s|%d|%d|%d|%s",
		appcode.Normalize(command.AppCode),
		command.TargetUserID,
		command.Amount,
		strings.TrimSpace(command.ApplicationID),
		strings.TrimSpace(command.CycleID),
		command.AgencyID,
		command.RankNo,
		command.ScoreCoins,
		strings.TrimSpace(command.Reason),
	))
}
