package grpc

import (
	"context"
	"time"

	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	walletservice "hyapp/services/wallet-service/internal/service/wallet"
)

// Server 把 wallet-service 用例层适配为 gRPC 接口。
type Server struct {
	walletv1.UnimplementedWalletServiceServer

	svc *walletservice.Service
}

// NewServer 创建 wallet gRPC server。
func NewServer(svc *walletservice.Service) *Server {
	return &Server{svc: svc}
}

// DebitGift 只负责 protobuf 转换和错误映射，扣费规则在 service/repository。
func (s *Server) DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.DebitGift(ctx, ledger.DebitGiftCommand{
		AppCode:        req.GetAppCode(),
		CommandID:      req.GetCommandId(),
		RoomID:         req.GetRoomId(),
		SenderUserID:   req.GetSenderUserId(),
		TargetUserID:   req.GetTargetUserId(),
		GiftID:         req.GetGiftId(),
		GiftCount:      req.GetGiftCount(),
		PriceVersion:   req.GetPriceVersion(),
		RegionID:       req.GetRegionId(),
		SenderRegionID: req.GetSenderRegionId(),
		TargetIsHost:   req.GetTargetIsHost(),
		// 工资政策区域来自 user-service host profile，不能用房间 visible_region_id 兜底。
		TargetHostRegionID:      req.GetTargetHostRegionId(),
		TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}

	return debitGiftResponseFromReceipt(receipt), nil
}

// BatchDebitGift 只负责批量目标 protobuf 转换；同事务扣费和幂等由 wallet service/repository 保证。
func (s *Server) BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	targets := make([]ledger.DebitGiftTargetCommand, 0, len(req.GetTargets()))
	for _, target := range req.GetTargets() {
		targets = append(targets, ledger.DebitGiftTargetCommand{
			CommandID:               target.GetCommandId(),
			TargetUserID:            target.GetTargetUserId(),
			TargetIsHost:            target.GetTargetIsHost(),
			TargetHostRegionID:      target.GetTargetHostRegionId(),
			TargetAgencyOwnerUserID: target.GetTargetAgencyOwnerUserId(),
		})
	}
	receipt, err := s.svc.BatchDebitGift(ctx, ledger.BatchDebitGiftCommand{
		AppCode:        req.GetAppCode(),
		CommandID:      req.GetCommandId(),
		RoomID:         req.GetRoomId(),
		SenderUserID:   req.GetSenderUserId(),
		GiftID:         req.GetGiftId(),
		GiftCount:      req.GetGiftCount(),
		PriceVersion:   req.GetPriceVersion(),
		RegionID:       req.GetRegionId(),
		SenderRegionID: req.GetSenderRegionId(),
		Targets:        targets,
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	response := &walletv1.BatchDebitGiftResponse{
		Aggregate: debitGiftResponseFromReceipt(receipt.Aggregate),
		Receipts:  make([]*walletv1.BatchDebitGiftReceipt, 0, len(receipt.Targets)),
	}
	for _, target := range receipt.Targets {
		response.Receipts = append(response.Receipts, &walletv1.BatchDebitGiftReceipt{
			TargetUserId: target.TargetUserID,
			CommandId:    target.CommandID,
			Billing:      debitGiftResponseFromReceipt(target.Receipt),
		})
	}
	return response, nil
}

func debitGiftResponseFromReceipt(receipt ledger.Receipt) *walletv1.DebitGiftResponse {
	return &walletv1.DebitGiftResponse{
		BillingReceiptId:       receipt.BillingReceiptID,
		BalanceAfter:           receipt.BalanceAfter,
		TransactionId:          receipt.TransactionID,
		CoinSpent:              receipt.CoinSpent,
		ChargeAssetType:        receipt.ChargeAssetType,
		ChargeAmount:           receipt.ChargeAmount,
		GiftPointAdded:         receipt.GiftPointAdded,
		HeatValue:              receipt.HeatValue,
		GiftTypeCode:           receipt.GiftTypeCode,
		CpRelationType:         receipt.CPRelationType,
		GiftName:               receipt.GiftName,
		GiftIconUrl:            receipt.GiftIconURL,
		GiftAnimationUrl:       receipt.GiftAnimationURL,
		PriceVersion:           receipt.PriceVersion,
		HostPeriodDiamondAdded: receipt.HostPeriodDiamondAdded,
		HostPeriodCycleKey:     receipt.HostPeriodCycleKey,
	}
}

// GetBalances 返回用户多资产余额投影。
func (s *Server) GetBalances(ctx context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	balances, err := s.svc.GetBalances(ctx, req.GetUserId(), req.GetAssetTypes())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}

	response := &walletv1.GetBalancesResponse{Balances: make([]*walletv1.AssetBalance, 0, len(balances))}
	for _, balance := range balances {
		response.Balances = append(response.Balances, &walletv1.AssetBalance{
			AppCode:         balance.AppCode,
			AssetType:       balance.AssetType,
			AvailableAmount: balance.AvailableAmount,
			FrozenAmount:    balance.FrozenAmount,
			Version:         balance.Version,
		})
	}

	return response, nil
}

// GetActiveHostSalaryPolicy 读取当前生效的主播工资政策，供 gateway 按当前主播区域展示平台规则。
func (s *Server) GetActiveHostSalaryPolicy(ctx context.Context, req *walletv1.GetActiveHostSalaryPolicyRequest) (*walletv1.GetActiveHostSalaryPolicyResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	policy, found, err := s.svc.GetActiveHostSalaryPolicy(ctx, req.GetAppCode(), req.GetRegionId(), req.GetSettlementMode(), req.GetSettlementTriggerMode(), req.GetNowMs())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	response := &walletv1.GetActiveHostSalaryPolicyResponse{Found: found}
	if found {
		response.Policy = hostSalaryPolicyToProto(policy)
	}
	return response, nil
}

// GetHostSalaryProgress 读取主播当前工资周期钻石累计，gateway 用它计算工资政策等级进度。
func (s *Server) GetHostSalaryProgress(ctx context.Context, req *walletv1.GetHostSalaryProgressRequest) (*walletv1.GetHostSalaryProgressResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	progress, err := s.svc.GetHostSalaryProgress(ctx, req.GetAppCode(), req.GetHostUserId(), req.GetCycleKey(), req.GetNowMs())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.GetHostSalaryProgressResponse{Progress: hostSalaryProgressToProto(progress)}, nil
}

func hostSalaryProgressToProto(progress ledger.HostSalaryProgress) *walletv1.HostSalaryProgress {
	return &walletv1.HostSalaryProgress{
		HostUserId:        progress.HostUserID,
		CycleKey:          progress.CycleKey,
		RegionId:          progress.RegionID,
		AgencyOwnerUserId: progress.AgencyOwnerUserID,
		TotalDiamonds:     progress.TotalDiamonds,
		GiftDiamondTotal:  progress.GiftDiamondTotal,
		UpdatedAtMs:       progress.UpdatedAtMS,
	}
}

func hostSalaryPolicyToProto(policy ledger.HostSalaryPolicy) *walletv1.HostSalaryPolicy {
	levels := make([]*walletv1.HostSalaryPolicyLevel, 0, len(policy.Levels))
	for _, level := range policy.Levels {
		levels = append(levels, hostSalaryPolicyLevelToProto(level))
	}
	return &walletv1.HostSalaryPolicy{
		PolicyId:                 policy.PolicyID,
		Name:                     policy.Name,
		RegionId:                 policy.RegionID,
		Status:                   policy.Status,
		SettlementMode:           policy.SettlementMode,
		SettlementTriggerMode:    policy.SettlementTriggerMode,
		GiftCoinToDiamondRatio:   policy.GiftCoinToDiamondRatio,
		ResidualDiamondToUsdRate: policy.ResidualDiamondToUSDRate,
		EffectiveFromMs:          policy.EffectiveFromMs,
		EffectiveToMs:            policy.EffectiveToMs,
		Levels:                   levels,
	}
}

func hostSalaryPolicyLevelToProto(level ledger.HostSalaryPolicyLevel) *walletv1.HostSalaryPolicyLevel {
	return &walletv1.HostSalaryPolicyLevel{
		LevelNo:              int32(level.LevelNo),
		RequiredDiamonds:     level.RequiredDiamonds,
		HostSalaryUsdMinor:   level.HostSalaryUSDMinor,
		HostCoinReward:       level.HostCoinReward,
		AgencySalaryUsdMinor: level.AgencySalaryUSDMinor,
		Status:               level.Status,
		SortOrder:            int32(level.SortOrder),
	}
}

// GetWalletOverview 返回钱包首页摘要，供我的页和钱包二级页复用。
func (s *Server) GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	overview, err := s.svc.GetWalletOverview(ctx, req.GetUserId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.GetWalletOverviewResponse{
		Balances:     balancesToProto(overview.Balances),
		FeatureFlags: walletFeatureFlagsToProto(overview.FeatureFlags),
	}, nil
}

// GetWalletValueSummary 返回我的页钱包卡片最小金币余额。
func (s *Server) GetWalletValueSummary(ctx context.Context, req *walletv1.GetWalletValueSummaryRequest) (*walletv1.GetWalletValueSummaryResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	summary, err := s.svc.GetWalletValueSummary(ctx, req.GetUserId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.GetWalletValueSummaryResponse{Summary: &walletv1.WalletValueSummary{
		CoinAmount:  summary.CoinAmount,
		UpdatedAtMs: summary.UpdatedAtMS,
	}}, nil
}

// GetUserGiftWall 返回当前用户收到礼物的聚合墙；调用方负责限定 user_id 为当前登录用户。
func (s *Server) GetUserGiftWall(ctx context.Context, req *walletv1.GetUserGiftWallRequest) (*walletv1.GetUserGiftWallResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	wall, err := s.svc.GetUserGiftWall(ctx, req.GetUserId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &walletv1.GetUserGiftWallResponse{
		Items:             make([]*walletv1.GiftWallItem, 0, len(wall.Items)),
		GiftKindCount:     wall.GiftKindCount,
		GiftTotalCount:    wall.GiftTotalCount,
		TotalValue:        wall.TotalValue,
		TotalCoinValue:    wall.TotalCoinValue,
		TotalDiamondValue: wall.TotalDiamondValue,
		TotalGiftPoint:    wall.TotalGiftPoint,
		TotalHeatValue:    wall.TotalHeatValue,
	}
	for _, item := range wall.Items {
		resp.Items = append(resp.Items, giftWallItemToProto(item))
	}
	return resp, nil
}

// ListRechargeProducts 返回当前用户区域可用的充值渠道和档位。
func (s *Server) ListRechargeProducts(ctx context.Context, req *walletv1.ListRechargeProductsRequest) (*walletv1.ListRechargeProductsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	products, channels, err := s.svc.ListRechargeProducts(ctx, req.GetUserId(), req.GetRegionId(), req.GetPlatform())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &walletv1.ListRechargeProductsResponse{Channels: channels, Products: make([]*walletv1.RechargeProduct, 0, len(products))}
	for _, product := range products {
		resp.Products = append(resp.Products, rechargeProductToProto(product))
	}
	return resp, nil
}

// ConfirmGooglePayment 校验 Google Play purchase token 并完成钱包入账。
func (s *Server) ConfirmGooglePayment(ctx context.Context, req *walletv1.ConfirmGooglePaymentRequest) (*walletv1.ConfirmGooglePaymentResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.ConfirmGooglePayment(ctx, ledger.GooglePaymentCommand{
		AppCode:        req.GetAppCode(),
		CommandID:      req.GetCommandId(),
		UserID:         req.GetUserId(),
		RegionID:       req.GetRegionId(),
		ProductID:      req.GetProductId(),
		ProductCode:    req.GetProductCode(),
		PackageName:    req.GetPackageName(),
		PurchaseToken:  req.GetPurchaseToken(),
		OrderID:        req.GetOrderId(),
		PurchaseTimeMS: req.GetPurchaseTimeMs(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.ConfirmGooglePaymentResponse{
		PaymentOrderId:   receipt.PaymentOrderID,
		TransactionId:    receipt.TransactionID,
		Status:           receipt.Status,
		ProductId:        receipt.ProductID,
		ProductCode:      receipt.ProductCode,
		CoinAmount:       receipt.CoinAmount,
		Balance:          balanceToProto(receipt.Balance),
		IdempotentReplay: receipt.IdempotentReplay,
		ConsumeState:     receipt.ConsumeState,
	}, nil
}

// ListAdminRechargeProducts 返回后台内购配置列表。
func (s *Server) ListAdminRechargeProducts(ctx context.Context, req *walletv1.ListAdminRechargeProductsRequest) (*walletv1.ListAdminRechargeProductsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	products, total, err := s.svc.ListAdminRechargeProducts(ctx, ledger.ListRechargeProductsQuery{
		AppCode:  req.GetAppCode(),
		Status:   req.GetStatus(),
		Platform: req.GetPlatform(),
		RegionID: req.GetRegionId(),
		Keyword:  req.GetKeyword(),
		Page:     req.GetPage(),
		PageSize: req.GetPageSize(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &walletv1.ListAdminRechargeProductsResponse{Products: make([]*walletv1.RechargeProduct, 0, len(products)), Total: total}
	for _, product := range products {
		resp.Products = append(resp.Products, rechargeProductToProto(product))
	}
	return resp, nil
}

// CreateRechargeProduct 创建后台内购商品配置。
func (s *Server) CreateRechargeProduct(ctx context.Context, req *walletv1.CreateRechargeProductRequest) (*walletv1.RechargeProductResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	product, err := s.svc.CreateRechargeProduct(ctx, ledger.RechargeProductCommand{
		AppCode:        req.GetAppCode(),
		AmountMicro:    req.GetAmountMicro(),
		CoinAmount:     req.GetCoinAmount(),
		ProductName:    req.GetProductName(),
		Description:    req.GetDescription(),
		Platform:       req.GetPlatform(),
		RegionIDs:      req.GetRegionIds(),
		Enabled:        req.GetEnabled(),
		OperatorUserID: req.GetOperatorUserId(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.RechargeProductResponse{Product: rechargeProductToProto(product)}, nil
}

// UpdateRechargeProduct 修改后台内购商品配置。
func (s *Server) UpdateRechargeProduct(ctx context.Context, req *walletv1.UpdateRechargeProductRequest) (*walletv1.RechargeProductResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	product, err := s.svc.UpdateRechargeProduct(ctx, ledger.RechargeProductCommand{
		AppCode:        req.GetAppCode(),
		ProductID:      req.GetProductId(),
		AmountMicro:    req.GetAmountMicro(),
		CoinAmount:     req.GetCoinAmount(),
		ProductName:    req.GetProductName(),
		Description:    req.GetDescription(),
		Platform:       req.GetPlatform(),
		RegionIDs:      req.GetRegionIds(),
		Enabled:        req.GetEnabled(),
		OperatorUserID: req.GetOperatorUserId(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.RechargeProductResponse{Product: rechargeProductToProto(product)}, nil
}

// DeleteRechargeProduct 删除后台内购商品配置。
func (s *Server) DeleteRechargeProduct(ctx context.Context, req *walletv1.DeleteRechargeProductRequest) (*walletv1.DeleteRechargeProductResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	if err := s.svc.DeleteRechargeProduct(ctx, req.GetAppCode(), req.GetProductId()); err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.DeleteRechargeProductResponse{Deleted: true}, nil
}

// GetDiamondExchangeConfig 返回钻石兑换配置。
func (s *Server) GetDiamondExchangeConfig(ctx context.Context, req *walletv1.GetDiamondExchangeConfigRequest) (*walletv1.GetDiamondExchangeConfigResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	rules, err := s.svc.GetDiamondExchangeConfig(ctx, req.GetUserId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &walletv1.GetDiamondExchangeConfigResponse{Rules: make([]*walletv1.DiamondExchangeRule, 0, len(rules))}
	for _, rule := range rules {
		resp.Rules = append(resp.Rules, &walletv1.DiamondExchangeRule{
			ExchangeType:  rule.ExchangeType,
			FromAssetType: rule.FromAssetType,
			ToAssetType:   rule.ToAssetType,
			FromAmount:    rule.FromAmount,
			ToAmount:      rule.ToAmount,
			Enabled:       rule.Enabled,
		})
	}
	return resp, nil
}

func rechargeProductToProto(product ledger.RechargeProduct) *walletv1.RechargeProduct {
	return &walletv1.RechargeProduct{
		AppCode:           product.AppCode,
		ProductId:         product.ProductID,
		ProductCode:       product.ProductCode,
		ProductName:       product.ProductName,
		Description:       product.Description,
		Platform:          product.Platform,
		Channel:           product.Channel,
		CurrencyCode:      product.CurrencyCode,
		AmountMicro:       product.AmountMicro,
		AmountMinor:       product.AmountMinor,
		CoinAmount:        product.CoinAmount,
		PolicyVersion:     product.PolicyVersion,
		Status:            product.Status,
		Enabled:           product.Enabled,
		SortOrder:         product.SortOrder,
		RegionIds:         product.RegionIDs,
		ResourceAssetType: product.ResourceAssetType,
		CreatedByUserId:   product.CreatedByUserID,
		UpdatedByUserId:   product.UpdatedByUserID,
		CreatedAtMs:       product.CreatedAtMS,
		UpdatedAtMs:       product.UpdatedAtMS,
	}
}

// ListWalletTransactions 返回当前用户钱包流水。
func (s *Server) ListWalletTransactions(ctx context.Context, req *walletv1.ListWalletTransactionsRequest) (*walletv1.ListWalletTransactionsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	items, total, err := s.svc.ListWalletTransactions(ctx, ledger.ListWalletTransactionsQuery{
		AppCode:   req.GetAppCode(),
		UserID:    req.GetUserId(),
		AssetType: req.GetAssetType(),
		Page:      req.GetPage(),
		PageSize:  req.GetPageSize(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &walletv1.ListWalletTransactionsResponse{Transactions: make([]*walletv1.WalletTransaction, 0, len(items)), Total: total}
	for _, item := range items {
		resp.Transactions = append(resp.Transactions, &walletv1.WalletTransaction{
			EntryId:            item.EntryID,
			TransactionId:      item.TransactionID,
			BizType:            item.BizType,
			AssetType:          item.AssetType,
			AvailableDelta:     item.AvailableDelta,
			FrozenDelta:        item.FrozenDelta,
			AvailableAfter:     item.AvailableAfter,
			FrozenAfter:        item.FrozenAfter,
			CounterpartyUserId: item.CounterpartyUserID,
			RoomId:             item.RoomID,
			CreatedAtMs:        item.CreatedAtMS,
		})
	}
	return resp, nil
}

// ListVipPackages 返回当前 VIP 和可购买包列表。
func (s *Server) ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	current, levels, err := s.svc.ListVipPackages(ctx, req.GetUserId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &walletv1.ListVipPackagesResponse{CurrentVip: vipToProto(current), Packages: make([]*walletv1.VipLevel, 0, len(levels))}
	for _, level := range levels {
		resp.Packages = append(resp.Packages, vipLevelToProto(level))
	}
	return resp, nil
}

// GetMyVip 返回当前登录用户 VIP 状态。
func (s *Server) GetMyVip(ctx context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	vip, err := s.svc.GetMyVip(ctx, req.GetUserId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.GetMyVipResponse{Vip: vipToProto(vip)}, nil
}

// PurchaseVip 购买、续期或升级 VIP。
func (s *Server) PurchaseVip(ctx context.Context, req *walletv1.PurchaseVipRequest) (*walletv1.PurchaseVipResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.PurchaseVip(ctx, ledger.PurchaseVipCommand{
		AppCode:   req.GetAppCode(),
		CommandID: req.GetCommandId(),
		UserID:    req.GetUserId(),
		Level:     req.GetLevel(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.PurchaseVipResponse{
		OrderId:          receipt.OrderID,
		TransactionId:    receipt.TransactionID,
		Vip:              vipToProto(receipt.Vip),
		CoinSpent:        receipt.CoinSpent,
		CoinBalanceAfter: receipt.CoinBalanceAfter,
		RewardItems:      vipRewardItemsToProto(receipt.RewardItems),
	}, nil
}

// DebitCPBreakupFee 扣除解除 CP/兄弟/姐妹关系费用；关系状态确认由 user-service 负责。
func (s *Server) DebitCPBreakupFee(ctx context.Context, req *walletv1.DebitCPBreakupFeeRequest) (*walletv1.DebitCPBreakupFeeResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.DebitCPBreakupFee(ctx, ledger.CPBreakupFeeCommand{
		AppCode:        req.GetAppCode(),
		CommandID:      req.GetCommandId(),
		UserID:         req.GetUserId(),
		RelationshipID: req.GetRelationshipId(),
		RelationType:   req.GetRelationType(),
		Amount:         req.GetAmount(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.DebitCPBreakupFeeResponse{
		TransactionId:    receipt.TransactionID,
		CoinSpent:        receipt.CoinSpent,
		CoinBalanceAfter: receipt.CoinBalanceAfter,
		Balance:          balanceToProto(receipt.Balance),
	}, nil
}

// GrantVip 统一处理活动和后台赠送 VIP。
func (s *Server) GrantVip(ctx context.Context, req *walletv1.GrantVipRequest) (*walletv1.GrantVipResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.GrantVip(ctx, ledger.GrantVipCommand{
		AppCode:        req.GetAppCode(),
		CommandID:      req.GetCommandId(),
		TargetUserID:   req.GetTargetUserId(),
		Level:          req.GetLevel(),
		GrantSource:    req.GetGrantSource(),
		OperatorUserID: req.GetOperatorUserId(),
		Reason:         req.GetReason(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.GrantVipResponse{
		TransactionId: receipt.TransactionID,
		Vip:           vipToProto(receipt.Vip),
		RewardItems:   vipRewardItemsToProto(receipt.RewardItems),
		ServerTimeMs:  time.Now().UnixMilli(),
	}, nil
}

// ListAdminVipLevels 返回后台 VIP 配置。
func (s *Server) ListAdminVipLevels(ctx context.Context, req *walletv1.ListAdminVipLevelsRequest) (*walletv1.ListAdminVipLevelsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	levels, err := s.svc.ListAdminVipLevels(ctx)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &walletv1.ListAdminVipLevelsResponse{Levels: make([]*walletv1.VipLevel, 0, len(levels)), ServerTimeMs: time.Now().UnixMilli()}
	for _, level := range levels {
		resp.Levels = append(resp.Levels, vipLevelToProto(level))
	}
	return resp, nil
}

// UpdateAdminVipLevels 保存后台 VIP 配置。
func (s *Server) UpdateAdminVipLevels(ctx context.Context, req *walletv1.UpdateAdminVipLevelsRequest) (*walletv1.UpdateAdminVipLevelsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	commands := make([]ledger.AdminVipLevelCommand, 0, len(req.GetLevels()))
	for _, item := range req.GetLevels() {
		if item == nil {
			continue
		}
		commands = append(commands, ledger.AdminVipLevelCommand{
			Level:                      item.GetLevel(),
			Name:                       item.GetName(),
			Status:                     item.GetStatus(),
			PriceCoin:                  item.GetPriceCoin(),
			DurationMS:                 item.GetDurationMs(),
			RewardResourceGroupID:      item.GetRewardResourceGroupId(),
			RequiredRechargeCoinAmount: item.GetRequiredRechargeCoinAmount(),
			SortOrder:                  item.GetSortOrder(),
		})
	}
	levels, err := s.svc.UpdateAdminVipLevels(ctx, commands, req.GetOperatorUserId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &walletv1.UpdateAdminVipLevelsResponse{Levels: make([]*walletv1.VipLevel, 0, len(levels)), ServerTimeMs: time.Now().UnixMilli()}
	for _, level := range levels {
		resp.Levels = append(resp.Levels, vipLevelToProto(level))
	}
	return resp, nil
}

// AdminCreditAsset 处理后台手动调账，公网权限和审计入口由 gateway/admin 层控制。
func (s *Server) AdminCreditAsset(ctx context.Context, req *walletv1.AdminCreditAssetRequest) (*walletv1.AdminCreditAssetResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	balance, transactionID, err := s.svc.AdminCreditAsset(ctx, ledger.AdminCreditAssetCommand{
		AppCode:        req.GetAppCode(),
		CommandID:      req.GetCommandId(),
		TargetUserID:   req.GetTargetUserId(),
		AssetType:      req.GetAssetType(),
		Amount:         req.GetAmount(),
		OperatorUserID: req.GetOperatorUserId(),
		Reason:         req.GetReason(),
		EvidenceRef:    req.GetEvidenceRef(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}

	return &walletv1.AdminCreditAssetResponse{
		TransactionId: transactionID,
		Balance: &walletv1.AssetBalance{
			AppCode:         balance.AppCode,
			AssetType:       balance.AssetType,
			AvailableAmount: balance.AvailableAmount,
			FrozenAmount:    balance.FrozenAmount,
			Version:         balance.Version,
		},
	}, nil
}

func balancesToProto(balances []ledger.AssetBalance) []*walletv1.AssetBalance {
	items := make([]*walletv1.AssetBalance, 0, len(balances))
	for _, balance := range balances {
		items = append(items, balanceToProto(balance))
	}
	return items
}

func balanceToProto(balance ledger.AssetBalance) *walletv1.AssetBalance {
	return &walletv1.AssetBalance{
		AppCode:         balance.AppCode,
		AssetType:       balance.AssetType,
		AvailableAmount: balance.AvailableAmount,
		FrozenAmount:    balance.FrozenAmount,
		Version:         balance.Version,
	}
}

func giftWallItemToProto(item ledger.GiftWallItem) *walletv1.GiftWallItem {
	return &walletv1.GiftWallItem{
		GiftId:               item.GiftID,
		GiftName:             item.GiftName,
		ResourceId:           item.ResourceID,
		Resource:             giftWallResourceToProto(item.Resource),
		ResourceSnapshotJson: item.ResourceSnapshotJSON,
		GiftTypeCode:         item.GiftTypeCode,
		PresentationJson:     item.PresentationJSON,
		GiftCount:            item.GiftCount,
		TotalValue:           item.TotalValue,
		TotalCoinValue:       item.TotalCoinValue,
		TotalDiamondValue:    item.TotalDiamondValue,
		TotalGiftPoint:       item.TotalGiftPoint,
		TotalHeatValue:       item.TotalHeatValue,
		ChargeAssetType:      item.ChargeAssetType,
		LastPriceVersion:     item.LastPriceVersion,
		FirstReceivedAtMs:    item.FirstReceivedAtMS,
		LastReceivedAtMs:     item.LastReceivedAtMS,
		SortOrder:            item.SortOrder,
	}
}

func giftWallResourceToProto(resource ledger.GiftWallResource) *walletv1.Resource {
	if resource.ResourceID == 0 {
		return nil
	}
	return &walletv1.Resource{
		AppCode:           resource.AppCode,
		ResourceId:        resource.ResourceID,
		ResourceCode:      resource.ResourceCode,
		ResourceType:      resource.ResourceType,
		Name:              resource.Name,
		Status:            resource.Status,
		Grantable:         resource.Grantable,
		GrantStrategy:     resource.GrantStrategy,
		WalletAssetType:   resource.WalletAssetType,
		WalletAssetAmount: resource.WalletAssetAmount,
		UsageScopes:       resource.UsageScopes,
		AssetUrl:          resource.AssetURL,
		PreviewUrl:        resource.PreviewURL,
		AnimationUrl:      resource.AnimationURL,
		MetadataJson:      resource.MetadataJSON,
		SortOrder:         resource.SortOrder,
		CreatedAtMs:       resource.CreatedAtMS,
		UpdatedAtMs:       resource.UpdatedAtMS,
	}
}

func walletFeatureFlagsToProto(flags ledger.WalletFeatureFlags) *walletv1.WalletFeatureFlags {
	return &walletv1.WalletFeatureFlags{
		RechargeEnabled:        flags.RechargeEnabled,
		DiamondExchangeEnabled: flags.DiamondExchangeEnabled,
	}
}

func vipToProto(vip ledger.UserVip) *walletv1.UserVip {
	return &walletv1.UserVip{
		UserId:      vip.UserID,
		Level:       vip.Level,
		Name:        vip.Name,
		Active:      vip.Active,
		StartedAtMs: vip.StartedAtMS,
		ExpiresAtMs: vip.ExpiresAtMS,
		UpdatedAtMs: vip.UpdatedAtMS,
	}
}

func vipLevelToProto(level ledger.VipLevel) *walletv1.VipLevel {
	return &walletv1.VipLevel{
		Level:                      level.Level,
		Name:                       level.Name,
		Status:                     level.Status,
		PriceCoin:                  level.PriceCoin,
		DurationMs:                 level.DurationMS,
		RewardResourceGroupId:      level.RewardResourceGroupID,
		RewardItems:                vipRewardItemsToProto(level.RewardItems),
		CanPurchase:                level.CanPurchase,
		SortOrder:                  level.SortOrder,
		RechargeGateRequired:       level.RechargeGateRequired,
		RequiredRechargeCoinAmount: level.RequiredRechargeCoinAmount,
		UserRechargeCoinAmount:     level.UserRechargeCoinAmount,
		PurchaseLockedReason:       level.PurchaseLockedReason,
		CreatedAtMs:                level.CreatedAtMS,
		UpdatedAtMs:                level.UpdatedAtMS,
	}
}

func vipRewardItemsToProto(items []ledger.VipRewardItem) []*walletv1.VipRewardItem {
	resp := make([]*walletv1.VipRewardItem, 0, len(items))
	for _, item := range items {
		resp = append(resp, &walletv1.VipRewardItem{
			ResourceId:   item.ResourceID,
			ResourceCode: item.ResourceCode,
			ResourceType: item.ResourceType,
			Name:         item.Name,
			Quantity:     item.Quantity,
			ExpiresAtMs:  item.ExpiresAtMS,
			AssetUrl:     item.AssetURL,
			PreviewUrl:   item.PreviewURL,
			AnimationUrl: item.AnimationURL,
		})
	}
	return resp
}

// CreditTaskReward 处理 activity-service 任务奖励金币入账。
func (s *Server) CreditTaskReward(ctx context.Context, req *walletv1.CreditTaskRewardRequest) (*walletv1.CreditTaskRewardResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.CreditTaskReward(ctx, ledger.TaskRewardCommand{
		AppCode:      req.GetAppCode(),
		CommandID:    req.GetCommandId(),
		TargetUserID: req.GetTargetUserId(),
		Amount:       req.GetAmount(),
		TaskType:     req.GetTaskType(),
		TaskID:       req.GetTaskId(),
		CycleKey:     req.GetCycleKey(),
		Reason:       req.GetReason(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}

	return &walletv1.CreditTaskRewardResponse{
		TransactionId: receipt.TransactionID,
		Amount:        receipt.Amount,
		GrantedAtMs:   receipt.GrantedAtMS,
		Balance: &walletv1.AssetBalance{
			AppCode:         receipt.Balance.AppCode,
			AssetType:       receipt.Balance.AssetType,
			AvailableAmount: receipt.Balance.AvailableAmount,
			FrozenAmount:    receipt.Balance.FrozenAmount,
			Version:         receipt.Balance.Version,
		},
	}, nil
}

// CreditLuckyGiftReward 处理 activity-service 幸运礼物抽奖 outbox 的中奖金币入账。
func (s *Server) CreditLuckyGiftReward(ctx context.Context, req *walletv1.CreditLuckyGiftRewardRequest) (*walletv1.CreditLuckyGiftRewardResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.CreditLuckyGiftReward(ctx, ledger.LuckyGiftRewardCommand{
		AppCode:         req.GetAppCode(),
		CommandID:       req.GetCommandId(),
		TargetUserID:    req.GetTargetUserId(),
		Amount:          req.GetAmount(),
		DrawID:          req.GetDrawId(),
		RoomID:          req.GetRoomId(),
		VisibleRegionID: req.GetVisibleRegionId(),
		GiftID:          req.GetGiftId(),
		PoolID:          req.GetPoolId(),
		Reason:          req.GetReason(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}

	return &walletv1.CreditLuckyGiftRewardResponse{
		TransactionId: receipt.TransactionID,
		Amount:        receipt.Amount,
		GrantedAtMs:   receipt.GrantedAtMS,
		Balance: &walletv1.AssetBalance{
			AppCode:         receipt.Balance.AppCode,
			AssetType:       receipt.Balance.AssetType,
			AvailableAmount: receipt.Balance.AvailableAmount,
			FrozenAmount:    receipt.Balance.FrozenAmount,
			Version:         receipt.Balance.Version,
		},
	}, nil
}

// CreditRoomTurnoverReward 处理 activity-service 每周房间流水奖励金币入账。
func (s *Server) CreditRoomTurnoverReward(ctx context.Context, req *walletv1.CreditRoomTurnoverRewardRequest) (*walletv1.CreditRoomTurnoverRewardResponse, error) {
	// gRPC 层不解释活动规则，只把 activity-service 的结算命令转换为钱包领域命令。
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.CreditRoomTurnoverReward(ctx, ledger.RoomTurnoverRewardCommand{
		AppCode:       req.GetAppCode(),
		CommandID:     req.GetCommandId(),
		TargetUserID:  req.GetTargetUserId(),
		Amount:        req.GetAmount(),
		SettlementID:  req.GetSettlementId(),
		RoomID:        req.GetRoomId(),
		PeriodStartMS: req.GetPeriodStartMs(),
		PeriodEndMS:   req.GetPeriodEndMs(),
		CoinSpent:     req.GetCoinSpent(),
		TierID:        req.GetTierId(),
		TierCode:      req.GetTierCode(),
		Reason:        req.GetReason(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}

	return &walletv1.CreditRoomTurnoverRewardResponse{
		TransactionId: receipt.TransactionID,
		Amount:        receipt.Amount,
		GrantedAtMs:   receipt.GrantedAtMS,
		Balance: &walletv1.AssetBalance{
			AppCode:         receipt.Balance.AppCode,
			AssetType:       receipt.Balance.AssetType,
			AvailableAmount: receipt.Balance.AvailableAmount,
			FrozenAmount:    receipt.Balance.FrozenAmount,
			Version:         receipt.Balance.Version,
		},
	}, nil
}

// ApplyGameCoinChange 处理 game-service 发起的游戏专用金币改账。
func (s *Server) ApplyGameCoinChange(ctx context.Context, req *walletv1.ApplyGameCoinChangeRequest) (*walletv1.ApplyGameCoinChangeResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.ApplyGameCoinChange(ctx, ledger.GameCoinChangeCommand{
		AppCode:         req.GetAppCode(),
		CommandID:       req.GetCommandId(),
		UserID:          req.GetUserId(),
		PlatformCode:    req.GetPlatformCode(),
		GameID:          req.GetGameId(),
		ProviderOrderID: req.GetProviderOrderId(),
		ProviderRoundID: req.GetProviderRoundId(),
		OpType:          req.GetOpType(),
		CoinAmount:      req.GetCoinAmount(),
		RoomID:          req.GetRoomId(),
		RequestHash:     req.GetRequestHash(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.ApplyGameCoinChangeResponse{
		WalletTransactionId: receipt.TransactionID,
		BalanceAfter:        receipt.BalanceAfter,
		IdempotentReplay:    receipt.IdempotentReplay,
	}, nil
}

// AdminCreditCoinSellerStock 处理后台币商进货或金币补偿，币商身份由 admin-server 先行校验。
func (s *Server) AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.AdminCreditCoinSellerStockRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.AdminCreditCoinSellerStock(ctx, ledger.CoinSellerStockCreditCommand{
		AppCode:          req.GetAppCode(),
		CommandID:        req.GetCommandId(),
		SellerUserID:     req.GetSellerUserId(),
		SellerCountryID:  req.GetSellerCountryId(),
		SellerRegionID:   req.GetSellerRegionId(),
		StockType:        req.GetStockType(),
		CoinAmount:       req.GetCoinAmount(),
		PaidCurrencyCode: req.GetPaidCurrencyCode(),
		PaidAmountMicro:  req.GetPaidAmountMicro(),
		PaymentRef:       req.GetPaymentRef(),
		EvidenceRef:      req.GetEvidenceRef(),
		OperatorUserID:   req.GetOperatorUserId(),
		Reason:           req.GetReason(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}

	return &walletv1.AdminCreditCoinSellerStockResponse{
		TransactionId:          receipt.TransactionID,
		SellerUserId:           receipt.SellerUserID,
		StockType:              receipt.StockType,
		CoinAmount:             receipt.CoinAmount,
		PaidCurrencyCode:       receipt.PaidCurrencyCode,
		PaidAmountMicro:        receipt.PaidAmountMicro,
		CountsAsSellerRecharge: receipt.CountsAsSellerRecharge,
		BalanceAfter:           receipt.BalanceAfter,
		CreatedAtMs:            receipt.CreatedAtMS,
	}, nil
}

// TransferCoinFromSeller 处理币商转玩家金币，调用方必须先完成币商身份校验。
func (s *Server) TransferCoinFromSeller(ctx context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.TransferCoinFromSeller(ctx, ledger.CoinSellerTransferCommand{
		AppCode:         req.GetAppCode(),
		CommandID:       req.GetCommandId(),
		SellerUserID:    req.GetSellerUserId(),
		TargetUserID:    req.GetTargetUserId(),
		TargetCountryID: req.GetTargetCountryId(),
		SellerRegionID:  req.GetSellerRegionId(),
		TargetRegionID:  req.GetTargetRegionId(),
		Amount:          req.GetAmount(),
		Reason:          req.GetReason(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}

	return &walletv1.TransferCoinFromSellerResponse{
		TransactionId:                receipt.TransactionID,
		SellerBalanceAfter:           receipt.SellerBalanceAfter,
		TargetBalanceAfter:           receipt.TargetBalanceAfter,
		Amount:                       receipt.Amount,
		RechargeUsdMinor:             receipt.RechargeUSDMinor,
		RechargeCurrencyCode:         receipt.RechargeCurrencyCode,
		RechargePolicyId:             receipt.RechargePolicyID,
		RechargePolicyVersion:        receipt.RechargePolicyVersion,
		RechargePolicyCoinAmount:     receipt.RechargePolicyCoinAmount,
		RechargePolicyUsdMinorAmount: receipt.RechargePolicyUSDMinorUnit,
	}, nil
}

// ListCoinSellerSalaryExchangeRateTiers 返回工资转币商使用的区域比例区间，调用方用它做预计到账展示。
func (s *Server) ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, req *walletv1.ListCoinSellerSalaryExchangeRateTiersRequest) (*walletv1.ListCoinSellerSalaryExchangeRateTiersResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	tiers, err := s.svc.ListCoinSellerSalaryExchangeRateTiers(ctx, req.GetAppCode(), req.GetRegionId(), req.GetIncludeDisabled())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	response := &walletv1.ListCoinSellerSalaryExchangeRateTiersResponse{Tiers: make([]*walletv1.CoinSellerSalaryExchangeRateTier, 0, len(tiers))}
	for _, tier := range tiers {
		response.Tiers = append(response.Tiers, &walletv1.CoinSellerSalaryExchangeRateTier{
			RegionId:    tier.RegionID,
			MinUsdMinor: tier.MinUSDMinor,
			MaxUsdMinor: tier.MaxUSDMinor,
			CoinPerUsd:  tier.CoinPerUSD,
			Status:      tier.Status,
			SortOrder:   int32(tier.SortOrder),
			UpdatedAtMs: tier.UpdatedAtMS,
		})
	}
	return response, nil
}

// ExchangeSalaryToCoin 处理用户工资兑换普通金币，身份资产已经由 gateway 校验并注入。
func (s *Server) ExchangeSalaryToCoin(ctx context.Context, req *walletv1.ExchangeSalaryToCoinRequest) (*walletv1.ExchangeSalaryToCoinResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.ExchangeSalaryToCoin(ctx, ledger.SalaryExchangeCommand{
		AppCode:         req.GetAppCode(),
		CommandID:       req.GetCommandId(),
		UserID:          req.GetUserId(),
		SalaryAssetType: req.GetSalaryAssetType(),
		SalaryUSDMinor:  req.GetSalaryUsdMinor(),
		Reason:          req.GetReason(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.ExchangeSalaryToCoinResponse{
		TransactionId:      receipt.TransactionID,
		SalaryBalanceAfter: receipt.SalaryBalanceAfter,
		CoinBalanceAfter:   receipt.CoinBalanceAfter,
		SalaryUsdMinor:     receipt.SalaryUSDMinor,
		CoinAmount:         receipt.CoinAmount,
		CoinPerUsd:         receipt.CoinPerUSD,
	}, nil
}

// TransferSalaryToCoinSeller 处理用户工资转同区域币商专用金币库存。
func (s *Server) TransferSalaryToCoinSeller(ctx context.Context, req *walletv1.TransferSalaryToCoinSellerRequest) (*walletv1.TransferSalaryToCoinSellerResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	receipt, err := s.svc.TransferSalaryToCoinSeller(ctx, ledger.SalaryTransferToCoinSellerCommand{
		AppCode:         req.GetAppCode(),
		CommandID:       req.GetCommandId(),
		SourceUserID:    req.GetSourceUserId(),
		SellerUserID:    req.GetSellerUserId(),
		SalaryAssetType: req.GetSalaryAssetType(),
		SalaryUSDMinor:  req.GetSalaryUsdMinor(),
		RegionID:        req.GetRegionId(),
		Reason:          req.GetReason(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &walletv1.TransferSalaryToCoinSellerResponse{
		TransactionId:            receipt.TransactionID,
		SourceSalaryBalanceAfter: receipt.SourceSalaryBalanceAfter,
		SellerBalanceAfter:       receipt.SellerBalanceAfter,
		SalaryUsdMinor:           receipt.SalaryUSDMinor,
		CoinAmount:               receipt.CoinAmount,
		CoinPerUsd:               receipt.CoinPerUSD,
		RateMinUsdMinor:          receipt.RateMinUSDMinor,
		RateMaxUsdMinor:          receipt.RateMaxUSDMinor,
	}, nil
}
