package commerce

import (
	"context"
	"crypto/rand"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const (
	pointsToCentsRate           = 1000
	spendEventServerPurchase    = "server_purchase"
	pointRewardStatusPending    = "pending"
	defaultSettlementFreezeDays = 3
)

type checkoutContext struct {
	cartItems  []CartItem
	products   map[int64]Product
	itemTotals map[int64]int
	total      int
}

type couponApplication struct {
	coupon           PromoCode
	orderAmount      int
	eligibleAmount   int
	discountAmount   int
	targetCartItemID *int64
	targetProductID  *int64
	itemDiscounts    map[int64]int
}

type createdInstance struct {
	instance   Instance
	baseAmount int
}

type agentSettlementCalc struct {
	costRate       int
	costAmount     int
	deltaAmount    int
	pointsAmount   int
	settlementType string
}

type SettlementReleaseSummary struct {
	Orders      int
	Settlements int
}

func (s *Service) ListAvailableCoupons(ctx context.Context, userID int64) (AvailableCouponsResponse, error) {
	if err := s.requireDB(); err != nil {
		return AvailableCouponsResponse{}, err
	}
	if active, err := s.isActiveAgent(ctx, userID); err != nil {
		return AvailableCouponsResponse{}, err
	} else if active {
		return AvailableCouponsResponse{Items: []AvailableCouponResponse{}, AgentDisabled: true}, nil
	}

	now := time.Now().UTC()
	var mallCoupons []PromoCode
	if err := s.db.WithContext(ctx).
		Model(&PromoCode{}).
		Joins("JOIN coupon_mall_claims ON coupon_mall_claims.promo_code_id = promo_codes.id").
		Joins("JOIN coupon_mall_items ON coupon_mall_items.promo_code_id = promo_codes.id").
		Where("coupon_mall_claims.user_id = ? AND coupon_mall_claims.status = ?", userID, "claimed").
		Where("coupon_mall_items.status = ?", "active").
		Where("promo_codes.is_enabled = ?", true).
		Where("(promo_codes.expires_at IS NULL OR promo_codes.expires_at > ?)", now).
		Find(&mallCoupons).Error; err != nil {
		return AvailableCouponsResponse{}, err
	}

	var ownCoupons []PromoCode
	if err := s.db.WithContext(ctx).
		Where("issuer_user_id = ? AND claim_method = ? AND is_enabled = ?", userID, "code", true).
		Where("(expires_at IS NULL OR expires_at > ?)", now).
		Find(&ownCoupons).Error; err != nil {
		return AvailableCouponsResponse{}, err
	}

	seen := map[int64]struct{}{}
	items := []AvailableCouponResponse{}
	for _, coupon := range append(mallCoupons, ownCoupons...) {
		if _, ok := seen[coupon.ID]; ok {
			continue
		}
		seen[coupon.ID] = struct{}{}
		if coupon.MaxUses > 0 && coupon.UsedCount >= coupon.MaxUses {
			continue
		}
		if coupon.MaxUsesPerUser > 0 {
			used, err := s.userCouponUsedCount(ctx, coupon.ID, userID)
			if err != nil {
				return AvailableCouponsResponse{}, err
			}
			if used >= coupon.MaxUsesPerUser {
				continue
			}
		}
		if coupon.NewUsersOnly {
			hasPrior, err := s.hasPriorPaidOrder(ctx, userID)
			if err != nil {
				return AvailableCouponsResponse{}, err
			}
			if hasPrior {
				continue
			}
		}
		if coupon.IssuerType != nil && *coupon.IssuerType == "agent" && coupon.IssuerUserID != nil && *coupon.IssuerUserID == userID {
			continue
		}
		items = append(items, newAvailableCouponResponse(coupon))
	}
	return AvailableCouponsResponse{Items: items, AgentDisabled: false}, nil
}

func (s *Service) ListOrders(ctx context.Context, userID int64) ([]OrderListItemResponse, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var orders []Order
	if err := s.db.WithContext(ctx).
		Where("user_id = ?", userID).
		Order("id DESC").
		Find(&orders).Error; err != nil {
		return nil, err
	}
	out := make([]OrderListItemResponse, 0, len(orders))
	for _, order := range orders {
		out = append(out, newOrderListItemResponse(order))
	}
	return out, nil
}

func (s *Service) GetOrder(ctx context.Context, userID int64, orderID int64) (OrderDetailResponse, error) {
	if err := s.requireDB(); err != nil {
		return OrderDetailResponse{}, err
	}
	var order Order
	err := s.db.WithContext(ctx).
		Where("id = ? AND user_id = ?", orderID, userID).
		First(&order).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return OrderDetailResponse{}, NotFound("订单不存在")
	}
	if err != nil {
		return OrderDetailResponse{}, err
	}
	var instances []Instance
	if err := s.db.WithContext(ctx).
		Where("order_id = ?", orderID).
		Find(&instances).Error; err != nil {
		return OrderDetailResponse{}, err
	}
	return newOrderDetailResponse(order, instances), nil
}

func (s *Service) PreviewCoupon(ctx context.Context, userID int64, req CouponPreviewRequest) (CouponPreviewResponse, error) {
	if err := s.requireDB(); err != nil {
		return CouponPreviewResponse{}, err
	}
	code, err := s.resolveCouponCode(ctx, req.CouponID, req.CouponCode)
	if err != nil {
		return CouponPreviewResponse{}, err
	}

	checkout, err := s.buildCheckoutContext(ctx, userID, req.CartItemIDs)
	if err != nil {
		return CouponPreviewResponse{}, err
	}

	if normalizeCouponCode(code) != "" && req.CouponTargetCartItemID == nil {
		selection, err := s.productCouponTargetSelection(ctx, userID, checkout, code)
		if err != nil {
			return CouponPreviewResponse{}, err
		}
		if selection != nil {
			return CouponPreviewResponse{
				TotalAmount:             checkout.total,
				CouponDiscountAmount:    0,
				PointsDiscountAmount:    0,
				DiscountAmount:          0,
				FinalAmount:             checkout.total,
				ActualUsePoints:         0,
				Coupon:                  selection.coupon,
				RequiresTargetSelection: true,
				TargetOptions:           selection.options,
			}, nil
		}
	}

	application, err := s.calculateCartCoupon(ctx, userID, checkout, code, req.CouponTargetCartItemID)
	if err != nil {
		return CouponPreviewResponse{}, err
	}
	couponDiscount := 0
	if application != nil {
		couponDiscount = application.discountAmount
	}
	afterCoupon := applyCouponToTotals(checkout.itemTotals, application)
	actualPoints, pointsDiscount, err := s.calculatePointsDiscount(ctx, userID, afterCoupon, req.UsePoints)
	if err != nil {
		return CouponPreviewResponse{}, err
	}
	finalAmount := maxInt(0, afterCoupon-pointsDiscount)
	var coupon *CouponApplicationResponse
	if application != nil {
		final := afterCoupon
		response := newCouponApplicationResponse(*application, &final)
		coupon = &response
	}
	return CouponPreviewResponse{
		TotalAmount:             checkout.total,
		CouponDiscountAmount:    couponDiscount,
		PointsDiscountAmount:    pointsDiscount,
		DiscountAmount:          couponDiscount + pointsDiscount,
		FinalAmount:             finalAmount,
		ActualUsePoints:         actualPoints,
		Coupon:                  coupon,
		RequiresTargetSelection: false,
		TargetOptions:           []CouponTargetOption{},
	}, nil
}

func (s *Service) Checkout(ctx context.Context, userID int64, req CheckoutRequest) (CheckoutResponse, error) {
	if err := s.requireDB(); err != nil {
		return CheckoutResponse{}, err
	}
	code, err := s.resolveCouponCode(ctx, req.CouponID, req.CouponCode)
	if err != nil {
		return CheckoutResponse{}, err
	}

	var out CheckoutResponse
	var emailSnapshot JSONRaw
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := &Service{db: tx, logger: s.logger}
		checkout, err := txService.buildCheckoutContext(ctx, userID, req.CartItemIDs)
		if err != nil {
			return err
		}
		if code != nil && strings.TrimSpace(*code) != "" {
			if err := txService.lockCouponForCheckout(ctx, *code); err != nil {
				return err
			}
		}
		application, err := txService.calculateCartCoupon(ctx, userID, checkout, code, req.CouponTargetCartItemID)
		if err != nil {
			return err
		}
		couponDiscount := 0
		if application != nil {
			couponDiscount = application.discountAmount
		}
		afterCoupon := applyCouponToTotals(checkout.itemTotals, application)
		actualPoints, pointsDiscount, err := txService.calculatePointsDiscount(ctx, userID, afterCoupon, req.UsePoints)
		if err != nil {
			return err
		}
		discountAmount := couponDiscount + pointsDiscount
		finalAmount := maxInt(0, afterCoupon-pointsDiscount)

		if err := txService.lockAndDecrementStock(ctx, checkout.cartItems); err != nil {
			return err
		}

		var user UserAccount
		err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", userID).First(&user).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return Unauthorized("Could not validate credentials")
		}
		if err != nil {
			return err
		}
		if user.Balance < finalAmount {
			return HTTPError{
				Status: 402,
				Detail: fmt.Sprintf("余额不足，需要 ¥%.2f，当前余额 ¥%.2f", float64(finalAmount)/100, float64(user.Balance)/100),
			}
		}
		balanceAfter := user.Balance - finalAmount
		if err := tx.Model(&UserAccount{}).Where("id = ?", userID).Update("balance", balanceAfter).Error; err != nil {
			return err
		}

		outOrderNo := generateOrderNo()
		affCode, err := txService.resolveOrderAffCode(ctx, user, application)
		if err != nil {
			return err
		}
		var promoCodeID *int64
		if application != nil {
			value := application.coupon.ID
			promoCodeID = &value
		}
		itemsSnapshot := buildItemsSnapshot(checkout, application)
		emailSnapshot = jsonRawFromAny(itemsSnapshot)
		order := Order{
			UserID:         userID,
			OutOrderNo:     outOrderNo,
			Status:         "paid",
			TotalAmount:    checkout.total,
			DiscountAmount: discountAmount,
			PromoCodeID:    promoCodeID,
			AffCode:        affCode,
			ItemsSnapshot:  emailSnapshot,
		}
		if err := tx.Create(&order).Error; err != nil {
			return err
		}
		if err := txService.calculateOrderSettlement(ctx, &order, userID, finalAmount); err != nil {
			return err
		}
		if err := txService.freezeAgentSubsidyForOrder(ctx, order); err != nil {
			return err
		}

		if application != nil {
			if err := txService.recordCouponUsage(ctx, *application, userID, order.ID); err != nil {
				return err
			}
		}
		if finalAmount > 0 {
			remark := fmt.Sprintf("购买服务 %s", outOrderNo)
			if err := tx.Create(&BalanceLog{
				UserID:       userID,
				ChangeType:   "order_pay",
				Delta:        -finalAmount,
				BalanceAfter: balanceAfter,
				RefNo:        &outOrderNo,
				Remark:       &remark,
			}).Error; err != nil {
				return err
			}
		}
		if actualPoints > 0 {
			if err := txService.usePointsForOrder(ctx, userID, actualPoints, order.ID, outOrderNo); err != nil {
				return err
			}
		}

		instances, err := txService.createInstances(ctx, userID, order.ID, checkout, application)
		if err != nil {
			return err
		}
		if err := txService.recordInstancePaidAmounts(ctx, instances, finalAmount, afterCoupon); err != nil {
			return err
		}
		if err := txService.createSelfPurchaseRewards(ctx, userID, order.ID, outOrderNo, instances, finalAmount, afterCoupon); err != nil {
			return err
		}
		if err := txService.recordServerPurchaseSpendEvent(ctx, userID, order.ID, outOrderNo, finalAmount, checkout.total, discountAmount); err != nil {
			return err
		}
		if err := tx.Where("id IN ? AND user_id = ?", req.CartItemIDs, userID).Delete(&CartItem{}).Error; err != nil {
			return err
		}

		var couponResponse *CouponApplicationResponse
		if application != nil {
			final := afterCoupon
			value := newCouponApplicationResponse(*application, &final)
			couponResponse = &value
		}
		out = CheckoutResponse{
			OrderID:              order.ID,
			OutOrderNo:           outOrderNo,
			TotalAmount:          checkout.total,
			DiscountAmount:       discountAmount,
			CouponDiscountAmount: couponDiscount,
			PointsDiscountAmount: pointsDiscount,
			PaidAmount:           finalAmount,
			Coupon:               couponResponse,
			InstanceCount:        len(instances),
		}
		return nil
	})
	if err != nil {
		return CheckoutResponse{}, err
	}
	if err := SendOrderPlacedEmail(ctx, s.db, userID, out.OutOrderNo, out.PaidAmount, emailSnapshot, s.plugins); err != nil && s.logger != nil {
		s.logger.Warn("order placed email skipped", "order_id", out.OrderID, "error", err)
	}
	return out, nil
}

func (s *Service) lockCouponForCheckout(ctx context.Context, code string) error {
	var coupon PromoCode
	err := s.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("code = ?", strings.TrimSpace(code)).
		First(&coupon).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return BadRequest("优惠券不存在")
	}
	return err
}

func (s *Service) resolveCouponCode(ctx context.Context, couponID *int64, couponCode *string) (*string, error) {
	if couponID != nil && *couponID > 0 {
		var coupon PromoCode
		err := s.db.WithContext(ctx).Where("id = ?", *couponID).First(&coupon).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("优惠券不存在")
		}
		if err != nil {
			return nil, err
		}
		return &coupon.Code, nil
	}
	if couponCode == nil {
		return nil, nil
	}
	normalized := normalizeCouponCode(couponCode)
	if normalized == "" {
		return nil, nil
	}
	return &normalized, nil
}

func (s *Service) buildCheckoutContext(ctx context.Context, userID int64, ids []int64) (checkoutContext, error) {
	if len(ids) == 0 {
		return checkoutContext{}, BadRequest("请选择要结算的商品")
	}
	var cartItems []CartItem
	if err := s.db.WithContext(ctx).
		Where("id IN ? AND user_id = ?", ids, userID).
		Find(&cartItems).Error; err != nil {
		return checkoutContext{}, err
	}
	if len(cartItems) != len(ids) {
		return checkoutContext{}, BadRequest("部分购物车项不存在或不属于当前用户")
	}

	productIDs := make([]int64, 0, len(cartItems))
	for _, item := range cartItems {
		productIDs = append(productIDs, item.ProductID)
	}
	var products []Product
	if err := s.db.WithContext(ctx).Where("id IN ?", uniqueInt64(productIDs)).Find(&products).Error; err != nil {
		return checkoutContext{}, err
	}
	byID := map[int64]Product{}
	for _, product := range products {
		byID[product.ID] = product
	}
	groupIDs := make([]int64, 0, len(products))
	for _, product := range products {
		groupIDs = append(groupIDs, product.GroupID)
	}
	var groups []ProductGroup
	if len(groupIDs) > 0 {
		if err := s.db.WithContext(ctx).Where("id IN ?", uniqueInt64(groupIDs)).Find(&groups).Error; err != nil {
			return checkoutContext{}, err
		}
	}
	groupByID := map[int64]ProductGroup{}
	for _, group := range groups {
		groupByID[group.ID] = group
	}
	requiresRealName := false
	for _, product := range products {
		group := groupByID[product.GroupID]
		if product.RequiresRealName || group.RequiresRealName {
			requiresRealName = true
			break
		}
	}
	if requiresRealName {
		var user UserAccount
		if err := s.db.WithContext(ctx).Select("id", "id_card_verified").Where("id = ?", userID).First(&user).Error; err != nil {
			return checkoutContext{}, err
		}
		if strings.ToLower(strings.TrimSpace(user.IDCardVerified)) != "verified" {
			return checkoutContext{}, HTTPError{Status: 403, Detail: map[string]any{
				"code":             "REAL_NAME_REQUIRED",
				"message":          "购买该产品前需要先完成实名认证",
				"verification_url": "/settings?tab=verification",
			}}
		}
	}

	itemTotals := map[int64]int{}
	total := 0
	for idx, item := range cartItems {
		product, ok := byID[item.ProductID]
		if !ok {
			return checkoutContext{}, BadRequest(fmt.Sprintf("产品 %d 不存在", item.ProductID))
		}
		if product.Status != "active" {
			return checkoutContext{}, BadRequest(fmt.Sprintf("产品「%s」已下架", product.Name))
		}
		if product.APIType == "plugin" {
			if product.APIIdentifier == nil || strings.TrimSpace(*product.APIIdentifier) == "" {
				return checkoutContext{}, BadRequest(fmt.Sprintf("plugin product %s is not bound to a provision plugin", product.Name))
			}
			if err := s.validatePluginCartFields(product, jsonObject(item.SliderValues)); err != nil {
				return checkoutContext{}, err
			}
		}
		if product.StockPolicy == "tracked" {
			available := product.Stock
			if available <= 0 {
				return checkoutContext{}, Conflict(fmt.Sprintf("产品「%s」已售罄", product.Name))
			}
			if available < item.Quantity {
				return checkoutContext{}, Conflict(fmt.Sprintf("产品「%s」库存不足，当前仅剩 %d 台", product.Name, available))
			}
		}
		pricing, err := s.calculateCartPricing(ctx, product, item.Cycle, item.Quantity, jsonObject(item.SliderValues))
		if err != nil {
			return checkoutContext{}, err
		}
		item.Cycle = pricing.Cycle
		item.UnitPrice = pricing.UnitPrice
		item.AddonPrice = pricing.AddonPrice
		item.SetupFee = pricing.SetupFee
		cartItems[idx] = item
		if err := s.db.WithContext(ctx).Model(&CartItem{}).Where("id = ?", item.ID).Updates(map[string]any{
			"cycle":       item.Cycle,
			"unit_price":  item.UnitPrice,
			"addon_price": item.AddonPrice,
			"setup_fee":   item.SetupFee,
		}).Error; err != nil {
			return checkoutContext{}, err
		}
		itemTotal := pricing.TotalCents
		itemTotals[item.ID] = itemTotal
		total += itemTotal
	}
	return checkoutContext{cartItems: cartItems, products: byID, itemTotals: itemTotals, total: total}, nil
}

func (s *Service) lockAndDecrementStock(ctx context.Context, items []CartItem) error {
	required := map[int64]int{}
	for _, item := range items {
		required[item.ProductID] += item.Quantity
	}
	if len(required) == 0 {
		return nil
	}
	productIDs := make([]int64, 0, len(required))
	for id := range required {
		productIDs = append(productIDs, id)
	}
	sort.Slice(productIDs, func(i, j int) bool { return productIDs[i] < productIDs[j] })

	var products []Product
	if err := s.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("id IN ?", productIDs).
		Order("id ASC").
		Find(&products).Error; err != nil {
		return err
	}
	byID := map[int64]Product{}
	for _, product := range products {
		byID[product.ID] = product
	}
	for _, productID := range productIDs {
		product, ok := byID[productID]
		if !ok {
			return BadRequest(fmt.Sprintf("产品 %d 不存在", productID))
		}
		if product.Status != "active" {
			return BadRequest(fmt.Sprintf("产品「%s」已下架", product.Name))
		}
		if product.StockPolicy != "tracked" {
			continue
		}
		available := product.Stock
		need := required[productID]
		if available <= 0 {
			return Conflict(fmt.Sprintf("产品「%s」已售罄", product.Name))
		}
		if available < need {
			return Conflict(fmt.Sprintf("产品「%s」库存不足，当前仅剩 %d 台", product.Name, available))
		}
		if err := s.db.WithContext(ctx).Model(&Product{}).Where("id = ?", productID).Update("stock", available-need).Error; err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) recordCouponUsage(ctx context.Context, application couponApplication, userID int64, orderID int64) error {
	now := time.Now().UTC()
	usage := PromoCodeUsage{
		PromoCodeID:    application.coupon.ID,
		CodeSnapshot:   application.coupon.Code,
		UserID:         userID,
		OrderID:        orderID,
		OrderAmount:    application.orderAmount,
		EligibleAmount: application.eligibleAmount,
		DiscountAmount: application.discountAmount,
		Status:         "used",
		UsedAt:         now,
		PayloadSnapshot: jsonRawFromAny(map[string]any{
			"type":                application.coupon.Type,
			"value":               application.coupon.Value,
			"description":         application.coupon.Description,
			"issuer_type":         application.coupon.IssuerType,
			"issuer_user_id":      application.coupon.IssuerUserID,
			"discount_level":      defaultString(application.coupon.DiscountLevel, "order"),
			"target_cart_item_id": application.targetCartItemID,
			"target_product_id":   application.targetProductID,
			"item_discounts":      application.itemDiscounts,
			"max_discount_amount": application.coupon.MaxDiscountAmount,
			"min_quantity":        application.coupon.MinQuantity,
		}),
	}
	if err := s.db.WithContext(ctx).Create(&usage).Error; err != nil {
		return err
	}
	if err := s.db.WithContext(ctx).
		Model(&PromoCode{}).
		Where("id = ?", application.coupon.ID).
		UpdateColumn("used_count", gorm.Expr("used_count + ?", 1)).Error; err != nil {
		return err
	}
	if application.coupon.ClaimMethod == "mall" {
		return s.db.WithContext(ctx).
			Model(&CouponMallClaim{}).
			Where("promo_code_id = ? AND user_id = ? AND status = ?", application.coupon.ID, userID, "claimed").
			Updates(map[string]any{"status": "used", "used_at": now, "order_id": orderID}).Error
	}
	return nil
}

func (s *Service) usePointsForOrder(ctx context.Context, userID int64, points int, orderID int64, orderNo string) error {
	var membership UserMembership
	err := s.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("user_id = ?", userID).
		First(&membership).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return BadRequest("积分不足")
	}
	if err != nil {
		return err
	}
	if membership.AvailablePoints < points {
		return BadRequest("积分不足")
	}
	after := membership.AvailablePoints - points
	if err := s.db.WithContext(ctx).Model(&UserMembership{}).Where("id = ?", membership.ID).Update("available_points", after).Error; err != nil {
		return err
	}
	remark := fmt.Sprintf("订单积分抵扣 %d 积分", points)
	return s.db.WithContext(ctx).Create(&PointLog{
		UserID:       userID,
		ChangeType:   "points_use",
		Delta:        -points,
		BalanceAfter: after,
		OrderID:      &orderID,
		RefNo:        &orderNo,
		Remark:       &remark,
	}).Error
}

func (s *Service) createInstances(ctx context.Context, userID int64, orderID int64, checkout checkoutContext, application *couponApplication) ([]createdInstance, error) {
	instances := []createdInstance{}
	discounts := map[int64]int{}
	if application != nil {
		discounts = application.itemDiscounts
	}
	for _, item := range checkout.cartItems {
		product := checkout.products[item.ProductID]
		unit := yuanToCents(item.UnitPrice) + yuanToCents(item.AddonPrice)
		memory := parseMemoryMB(product.Memory)
		autoRenew := 0
		if item.AutoRenew {
			autoRenew = 1
		}
		itemTotal := maxInt(0, checkout.itemTotals[item.ID])
		couponDiscount := minInt(itemTotal, maxInt(0, discounts[item.ID]))
		perInstanceAmounts := splitAmount(maxInt(0, itemTotal-couponDiscount), item.Quantity)
		for i := 0; i < item.Quantity; i++ {
			baseAmount := 0
			if i < len(perInstanceAmounts) {
				baseAmount = perInstanceAmounts[i]
			}
			instance := Instance{
				UserID:          userID,
				OrderID:         orderID,
				ProductID:       item.ProductID,
				CloudServerID:   product.CloudServerID,
				Status:          "provisioning",
				IsLocked:        0,
				Internet:        1,
				Cycle:           item.Cycle,
				UnitPrice:       unit,
				AutoRenew:       autoRenew,
				NextRenewAmount: unit,
				Alias:           item.Alias,
				OSImage:         item.OSImage,
				CPU:             product.VCPU,
				Memory:          memory,
				UpstreamPayload: checkoutInstanceState(product, baseAmount),
			}
			if err := s.db.WithContext(ctx).Create(&instance).Error; err != nil {
				return nil, err
			}
			instances = append(instances, createdInstance{instance: instance, baseAmount: baseAmount})
		}
	}
	return instances, nil
}

func (s *Service) createSelfPurchaseRewards(ctx context.Context, userID int64, orderID int64, orderNo string, instances []createdInstance, finalAmount int, afterCoupon int) error {
	if len(instances) == 0 || finalAmount <= 0 || afterCoupon <= 0 {
		return nil
	}
	baseAmounts := make([]int, 0, len(instances))
	for _, item := range instances {
		baseAmounts = append(baseAmounts, item.baseAmount)
	}
	finalAmounts := allocateFinalAmounts(baseAmounts, finalAmount, afterCoupon)
	for idx, item := range instances {
		amount := 0
		if idx < len(finalAmounts) {
			amount = finalAmounts[idx]
		}
		if amount <= 0 {
			continue
		}
		if err := s.createSelfPurchasePointReward(ctx, userID, orderID, orderNo, item.instance.ID, amount, spendEventServerPurchase, pointRewardStatusPending); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) recordInstancePaidAmounts(ctx context.Context, instances []createdInstance, finalAmount int, afterCoupon int) error {
	if len(instances) == 0 {
		return nil
	}
	baseAmounts := make([]int, 0, len(instances))
	for _, item := range instances {
		baseAmounts = append(baseAmounts, item.baseAmount)
	}
	paidAmounts := allocateFinalAmounts(baseAmounts, finalAmount, afterCoupon)
	for index, item := range instances {
		paidAmount := 0
		if index < len(paidAmounts) {
			paidAmount = paidAmounts[index]
		}
		payload := jsonMap(item.instance.UpstreamPayload)
		if payload == nil {
			payload = map[string]any{}
		}
		payload["local_billing"] = map[string]any{"paid_amount_cents": maxInt(0, paidAmount)}
		if err := s.db.WithContext(ctx).Model(&Instance{}).Where("id = ?", item.instance.ID).Update("upstream_payload", jsonRawFromAny(payload)).Error; err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) createSelfPurchasePointReward(ctx context.Context, userID int64, orderID int64, orderNo string, instanceID int64, amountCents int, sourceType string, status string) error {
	if sourceType == "" {
		sourceType = spendEventServerPurchase
	}
	if status == "" {
		status = pointRewardStatusPending
	}
	var existing PointReward
	err := s.db.WithContext(ctx).
		Where("order_id = ? AND instance_id = ? AND source_type = ?", orderID, instanceID, sourceType).
		First(&existing).Error
	if err == nil {
		return nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	points, cashbackRate, rewardCents, err := s.calculateSelfPurchaseReward(ctx, userID, amountCents)
	if err != nil {
		return err
	}
	if points <= 0 {
		return nil
	}
	remark := fmt.Sprintf("self purchase points reward, rate %.2f%%, claimable after provisioning", float64(cashbackRate)/100)
	payload := jsonRawFromAny(map[string]any{
		"reward_cents": rewardCents,
		"instance_id":  instanceID,
	})
	reward := PointReward{
		UserID:          userID,
		OrderID:         orderID,
		InstanceID:      &instanceID,
		SourceType:      sourceType,
		Status:          status,
		AmountCents:     amountCents,
		RewardRate:      cashbackRate,
		Points:          points,
		RefNo:           &orderNo,
		Remark:          &remark,
		PayloadSnapshot: payload,
	}
	return s.db.WithContext(ctx).Create(&reward).Error
}

func (s *Service) calculateSelfPurchaseReward(ctx context.Context, userID int64, amountCents int) (int, int, int, error) {
	if amountCents <= 0 {
		return 0, 0, 0, nil
	}
	membership, err := s.getOrCreateMembership(ctx, userID)
	if err != nil {
		return 0, 0, 0, err
	}
	cashbackRate := 0
	if membership.MemberLevelID != nil {
		var level MemberLevel
		err := s.db.WithContext(ctx).Where("id = ?", *membership.MemberLevelID).First(&level).Error
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, 0, 0, err
		}
		if err == nil {
			cashbackRate = level.PointsCashbackRate
		}
	}
	points, rewardCents := selfPurchaseReward(amountCents, cashbackRate)
	return points, cashbackRate, rewardCents, nil
}

func (s *Service) getOrCreateMembership(ctx context.Context, userID int64) (UserMembership, error) {
	var membership UserMembership
	err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&membership).Error
	if err == nil {
		return membership, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return UserMembership{}, err
	}
	var level MemberLevel
	err = s.db.WithContext(ctx).
		Where("level = ? AND enabled = ?", 0, true).
		First(&level).Error
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return UserMembership{}, err
	}
	membership = UserMembership{UserID: userID}
	if err == nil {
		membership.MemberLevelID = &level.ID
	}
	if err := s.db.WithContext(ctx).Create(&membership).Error; err != nil {
		return UserMembership{}, err
	}
	return membership, nil
}

func (s *Service) recordServerPurchaseSpendEvent(ctx context.Context, userID int64, orderID int64, orderNo string, finalAmount int, totalAmount int, discountAmount int) error {
	if finalAmount == 0 {
		return nil
	}
	if err := s.ensureSpendEventTypeConfig(ctx, spendEventServerPurchase); err != nil {
		return err
	}
	eventKey := orderNo + ":server_purchase"
	var existing UserSpendEvent
	err := s.db.WithContext(ctx).Where("event_key = ?", eventKey).First(&existing).Error
	if err == nil {
		return s.recalculateMembershipSpent(ctx, userID)
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	remark := fmt.Sprintf("purchase service %s", orderNo)
	payload := jsonRawFromAny(map[string]any{
		"total_amount":    totalAmount,
		"discount_amount": discountAmount,
		"final_amount":    finalAmount,
	})
	event := UserSpendEvent{
		UserID:          userID,
		EventType:       spendEventServerPurchase,
		EventKey:        eventKey,
		AmountCents:     finalAmount,
		OrderID:         &orderID,
		RefNo:           &orderNo,
		Remark:          &remark,
		PayloadSnapshot: payload,
	}
	if err := s.db.WithContext(ctx).Create(&event).Error; err != nil {
		return err
	}
	return s.recalculateMembershipSpent(ctx, userID)
}

func (s *Service) FreezeOrderSettlements(ctx context.Context, orderID int64) error {
	var order Order
	err := s.db.WithContext(ctx).Where("id = ?", orderID).First(&order).Error
	if errors.Is(err, gorm.ErrRecordNotFound) || order.Status != "paid" {
		return nil
	}
	if err != nil {
		return err
	}
	var count int64
	if err := s.db.WithContext(ctx).
		Model(&AgentSettlement{}).
		Where("order_id = ? AND status IN ?", order.ID, []string{"pending", "frozen"}).
		Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return nil
	}
	freezeDays := s.settlementFreezeDays(ctx)
	frozenUntil := settlementReleaseAt(order.CreatedAt, freezeDays)
	remark := "全额退款窗口内冻结，窗口结束后自动释放"
	if err := s.db.WithContext(ctx).
		Model(&AgentSettlement{}).
		Where("order_id = ? AND status IN ?", order.ID, []string{"pending", "frozen"}).
		Updates(map[string]any{
			"status":       "frozen",
			"frozen_until": frozenUntil,
			"remark":       remark,
		}).Error; err != nil {
		return err
	}
	status := "frozen"
	return s.db.WithContext(ctx).Model(&Order{}).Where("id = ?", order.ID).Update("settlement_status", status).Error
}

func (s *Service) CreateMemberCommissionSettlementForOrder(ctx context.Context, orderID int64) error {
	var order Order
	err := s.db.WithContext(ctx).Where("id = ?", orderID).First(&order).Error
	if errors.Is(err, gorm.ErrRecordNotFound) || order.Status != "paid" {
		return nil
	}
	if err != nil {
		return err
	}
	var existing int64
	if err := s.db.WithContext(ctx).
		Model(&AgentSettlement{}).
		Where("order_id = ? AND settlement_type = ?", order.ID, "member_commission").
		Count(&existing).Error; err != nil {
		return err
	}
	if existing > 0 {
		return nil
	}
	if err := s.db.WithContext(ctx).
		Model(&PointLog{}).
		Where("order_id = ? AND change_type = ?", order.ID, "referral_reward").
		Count(&existing).Error; err != nil {
		return err
	}
	if existing > 0 {
		return nil
	}

	var customer UserAccount
	err = s.db.WithContext(ctx).Where("id = ?", order.UserID).First(&customer).Error
	if errors.Is(err, gorm.ErrRecordNotFound) || customer.BoundAgentID == nil || *customer.BoundAgentID == order.UserID {
		return nil
	}
	if err != nil {
		return err
	}
	active, err := s.isActiveAgent(ctx, *customer.BoundAgentID)
	if err != nil {
		return err
	}
	if active {
		return nil
	}

	membership, err := s.getOrCreateMembership(ctx, *customer.BoundAgentID)
	if err != nil {
		return err
	}
	if membership.MemberLevelID == nil {
		return nil
	}
	var level MemberLevel
	err = s.db.WithContext(ctx).Where("id = ?", *membership.MemberLevelID).First(&level).Error
	if errors.Is(err, gorm.ErrRecordNotFound) || level.CommissionRate <= 0 {
		return nil
	}
	if err != nil {
		return err
	}

	payAmount := maxInt(0, order.TotalAmount-order.DiscountAmount)
	rewardCents := payAmount * level.CommissionRate / 10000
	points := rewardCents * 10
	if points <= 0 {
		return nil
	}
	status := "pending"
	order.MemberCommissionRate = &level.CommissionRate
	order.MemberCommissionAmount = &rewardCents
	order.SettlementStatus = &status
	if err := s.db.WithContext(ctx).Model(&Order{}).Where("id = ?", order.ID).Updates(map[string]any{
		"member_commission_rate":   level.CommissionRate,
		"member_commission_amount": rewardCents,
		"settlement_status":        status,
	}).Error; err != nil {
		return err
	}
	remark := fmt.Sprintf("普通推广奖励，实付%d分，比例%.2f%%，预计%d积分", payAmount, float64(level.CommissionRate)/100, points)
	return s.db.WithContext(ctx).Create(&AgentSettlement{
		OrderID:           order.ID,
		OrderNo:           order.OutOrderNo,
		SettlementType:    "member_commission",
		UserID:            *customer.BoundAgentID,
		CustomerUserID:    &order.UserID,
		StandardAmount:    order.TotalAmount,
		CustomerPayAmount: payAmount,
		CommissionRate:    &level.CommissionRate,
		DeltaAmount:       rewardCents,
		PointsAmount:      points,
		Status:            status,
		Remark:            &remark,
	}).Error
}

func (s *Service) SettleDueRewards(ctx context.Context, limit int) (SettlementReleaseSummary, error) {
	if limit <= 0 {
		limit = 100
	}
	now := time.Now().UTC()
	orderIDs := []int64{}
	if err := s.db.WithContext(ctx).
		Model(&Order{}).
		Select("DISTINCT orders.id").
		Joins("JOIN agent_settlements ON agent_settlements.order_id = orders.id").
		Where("agent_settlements.status = ? AND agent_settlements.frozen_until IS NOT NULL AND agent_settlements.frozen_until <= ? AND orders.status = ?", "frozen", now, "paid").
		Order("agent_settlements.frozen_until ASC, orders.id ASC").
		Limit(limit).
		Pluck("orders.id", &orderIDs).Error; err != nil {
		return SettlementReleaseSummary{}, err
	}
	summary := SettlementReleaseSummary{Orders: len(orderIDs)}
	for _, orderID := range orderIDs {
		count, err := s.SettleOrder(ctx, orderID, true)
		if err != nil {
			return summary, err
		}
		summary.Settlements += count
	}
	return summary, nil
}

func (s *Service) SettleOrder(ctx context.Context, orderID int64, force bool) (int, error) {
	settled := 0
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := &Service{db: tx, logger: s.logger}
		var order Order
		err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", orderID).First(&order).Error
		if errors.Is(err, gorm.ErrRecordNotFound) || order.Status != "paid" {
			return nil
		}
		if err != nil {
			return err
		}
		var settlements []AgentSettlement
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("order_id = ? AND status IN ?", order.ID, []string{"pending", "frozen"}).
			Order("id ASC").
			Find(&settlements).Error; err != nil {
			return err
		}
		if len(settlements) == 0 {
			return nil
		}

		now := time.Now().UTC()
		releaseAt := settlementReleaseAt(order.CreatedAt, txService.settlementFreezeDays(ctx))
		shouldFreeze := !force && now.Before(releaseAt)
		for idx := range settlements {
			settlement := &settlements[idx]
			settlement.PointsAmount = maxInt(0, settlement.DeltaAmount*10)
			if shouldFreeze {
				settlement.Status = "frozen"
				settlement.FrozenUntil = &releaseAt
				remark := "全额退款窗口内冻结，窗口结束后自动释放"
				settlement.Remark = &remark
				if err := tx.Save(settlement).Error; err != nil {
					return err
				}
				continue
			}
			if err := txService.settleOneSettlement(ctx, &order, settlement, now); err != nil {
				return err
			}
			settled++
		}
		status := "settled"
		if shouldFreeze {
			status = "frozen"
		}
		return tx.Model(&Order{}).Where("id = ?", order.ID).Update("settlement_status", status).Error
	})
	return settled, err
}

func (s *Service) settlementFreezeDays(ctx context.Context) int {
	var item SystemSetting
	err := s.db.WithContext(ctx).Where("`key` = ?", "refund.full_refund_days").First(&item).Error
	if err != nil {
		return defaultSettlementFreezeDays
	}
	return settingInt(item.Value, defaultSettlementFreezeDays)
}

func (s *Service) ensureSpendEventTypeConfig(ctx context.Context, code string) error {
	var config SpendEventTypeConfig
	err := s.db.WithContext(ctx).Where("code = ?", code).First(&config).Error
	if err == nil {
		return nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	description := "Server purchase effective spend"
	config = SpendEventTypeConfig{
		Code:        code,
		Label:       "Server purchase",
		Description: &description,
		Enabled:     true,
		SortOrder:   10,
	}
	return s.db.WithContext(ctx).Create(&config).Error
}

func (s *Service) recalculateMembershipSpent(ctx context.Context, userID int64) error {
	membership, err := s.getOrCreateMembership(ctx, userID)
	if err != nil {
		return err
	}
	var total int64
	if err := s.db.WithContext(ctx).
		Model(&UserSpendEvent{}).
		Joins("JOIN spend_event_type_configs ON spend_event_type_configs.code = user_spend_events.event_type").
		Where("user_spend_events.user_id = ? AND spend_event_type_configs.enabled = ?", userID, true).
		Select("COALESCE(SUM(user_spend_events.amount_cents), 0)").
		Scan(&total).Error; err != nil {
		return err
	}
	updates := map[string]any{"total_spent": maxInt(0, int(total))}
	if !membership.MemberLevelLocked {
		var quarterTotal int64
		if err := s.db.WithContext(ctx).
			Model(&UserSpendEvent{}).
			Joins("JOIN spend_event_type_configs ON spend_event_type_configs.code = user_spend_events.event_type").
			Where("user_spend_events.user_id = ? AND user_spend_events.created_at >= ? AND spend_event_type_configs.enabled = ?", userID, currentQuarterStart(time.Now()), true).
			Select("COALESCE(SUM(user_spend_events.amount_cents), 0)").
			Scan(&quarterTotal).Error; err != nil {
			return err
		}
		var level MemberLevel
		err := s.db.WithContext(ctx).
			Where("enabled = ? AND min_total_spent <= ?", true, maxInt(0, int(quarterTotal))).
			Order("level DESC").
			First(&level).Error
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		if err == nil && (membership.MemberLevelID == nil || *membership.MemberLevelID != level.ID) {
			now := time.Now().UTC()
			updates["member_level_id"] = level.ID
			updates["upgraded_at"] = now
		}
	}
	return s.db.WithContext(ctx).Model(&UserMembership{}).Where("id = ?", membership.ID).Updates(updates).Error
}

func (s *Service) resolveOrderAffCode(ctx context.Context, user UserAccount, application *couponApplication) (*string, error) {
	var agentID *int64
	if application != nil && application.coupon.IssuerType != nil && *application.coupon.IssuerType == "agent" {
		agentID = application.coupon.IssuerUserID
	}
	if agentID == nil {
		agentID = user.BoundAgentID
	}
	if agentID == nil {
		return nil, nil
	}
	var agent UserAccount
	err := s.db.WithContext(ctx).Where("id = ?", *agentID).First(&agent).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if agent.InviteCode == nil || strings.TrimSpace(*agent.InviteCode) == "" {
		return nil, nil
	}
	code := strings.ToUpper(strings.TrimSpace(*agent.InviteCode))
	return &code, nil
}

func (s *Service) calculateOrderSettlement(ctx context.Context, order *Order, customerID int64, customerPayAmount int) error {
	var customer UserAccount
	err := s.db.WithContext(ctx).Where("id = ?", customerID).First(&customer).Error
	if errors.Is(err, gorm.ErrRecordNotFound) || customer.BoundAgentID == nil || *customer.BoundAgentID == customerID {
		return nil
	}
	if err != nil {
		return err
	}

	var agent UserAgent
	err = s.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("user_id = ? AND status IN ?", *customer.BoundAgentID, []string{"active", "observing"}).
		First(&agent).Error
	if errors.Is(err, gorm.ErrRecordNotFound) || agent.AgentTierID == nil {
		return nil
	}
	if err != nil {
		return err
	}

	var tier AgentTier
	err = s.db.WithContext(ctx).Where("id = ?", *agent.AgentTierID).First(&tier).Error
	if errors.Is(err, gorm.ErrRecordNotFound) || tier.CostRate <= 0 {
		return nil
	}
	if err != nil {
		return err
	}

	snapshot := agentSettlementSnapshot(order.TotalAmount, customerPayAmount, tier.CostRate)
	status := "pending"
	order.AgentID = &agent.UserID
	order.AgentTierID = &tier.ID
	order.AgentCostRate = &snapshot.costRate
	order.AgentCostAmount = &snapshot.costAmount
	order.AgentDeltaAmount = &snapshot.deltaAmount
	order.SettlementStatus = &status
	if err := s.db.WithContext(ctx).Model(order).Updates(map[string]any{
		"agent_id":           order.AgentID,
		"agent_tier_id":      order.AgentTierID,
		"agent_cost_rate":    order.AgentCostRate,
		"agent_cost_amount":  order.AgentCostAmount,
		"agent_delta_amount": order.AgentDeltaAmount,
		"settlement_status":  status,
	}).Error; err != nil {
		return err
	}

	settlement := AgentSettlement{
		OrderID:           order.ID,
		OrderNo:           order.OutOrderNo,
		SettlementType:    snapshot.settlementType,
		UserID:            agent.UserID,
		CustomerUserID:    &customerID,
		AgentTierID:       &tier.ID,
		StandardAmount:    order.TotalAmount,
		CustomerPayAmount: customerPayAmount,
		AgentCostRate:     &snapshot.costRate,
		AgentCostAmount:   &snapshot.costAmount,
		DeltaAmount:       snapshot.deltaAmount,
		PointsAmount:      snapshot.pointsAmount,
		Status:            status,
	}
	if err := s.db.WithContext(ctx).Create(&settlement).Error; err != nil {
		return err
	}

	updates := map[string]any{
		"total_sales": agent.TotalSales + order.TotalAmount,
	}
	if snapshot.deltaAmount > 0 {
		updates["total_profit"] = agent.TotalProfit + snapshot.deltaAmount
	} else if snapshot.deltaAmount < 0 {
		updates["total_subsidy"] = agent.TotalSubsidy + absInt(snapshot.deltaAmount)
	}
	return s.db.WithContext(ctx).Model(&UserAgent{}).Where("id = ?", agent.ID).Updates(updates).Error
}

func (s *Service) freezeAgentSubsidyForOrder(ctx context.Context, order Order) error {
	if order.AgentID == nil || order.AgentDeltaAmount == nil || *order.AgentDeltaAmount >= 0 {
		return nil
	}
	amount := absInt(*order.AgentDeltaAmount)
	var agent UserAgent
	err := s.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("user_id = ?", *order.AgentID).
		First(&agent).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return BadRequest("代理身份不存在，无法冻结补贴")
	}
	if err != nil {
		return err
	}
	if agent.AgentBalance < amount {
		return BadRequest("代理可用余额不足，无法补贴该订单")
	}
	balanceAfter := agent.AgentBalance - amount
	frozenAfter := agent.AgentFrozenBalance + amount
	if err := s.db.WithContext(ctx).Model(&UserAgent{}).Where("id = ?", agent.ID).Updates(map[string]any{
		"agent_balance":        balanceAfter,
		"agent_frozen_balance": frozenAfter,
	}).Error; err != nil {
		return err
	}
	remark := fmt.Sprintf("订单 %s 冻结代理补贴 %d 分", order.OutOrderNo, amount)
	return s.db.WithContext(ctx).Create(&AgentBalanceLog{
		UserID:             *order.AgentID,
		ChangeType:         "subsidy_freeze",
		Delta:              -amount,
		BalanceAfter:       balanceAfter,
		FrozenBalanceAfter: frozenAfter,
		OrderID:            &order.ID,
		RefNo:              &order.OutOrderNo,
		Remark:             &remark,
	}).Error
}

func (s *Service) settleOneSettlement(ctx context.Context, order *Order, settlement *AgentSettlement, now time.Time) error {
	if settlement.SettlementType == "agent_subsidy" {
		return s.chargeAgentSubsidy(ctx, order, settlement, now)
	}
	if settlement.DeltaAmount <= 0 {
		settlement.Status = "settled"
		settlement.SettledAt = &now
		remark := "非正向收益，无积分发放"
		settlement.Remark = &remark
		return s.db.WithContext(ctx).Save(settlement).Error
	}
	if settlement.PointLogID != nil {
		settlement.Status = "settled"
		settlement.SettledAt = &now
		return s.db.WithContext(ctx).Save(settlement).Error
	}
	points := maxInt(0, settlement.DeltaAmount*10)
	if settlement.SettlementType == "agent_profit" {
		if err := s.creditAgentProfit(ctx, order, settlement); err != nil {
			return err
		}
		log, err := s.addPoints(ctx, settlement.UserID, points, "agent_profit", order.ID, order.OutOrderNo, fmt.Sprintf("agent_profit order %s settled: %d cents -> %d points", settlement.OrderNo, settlement.DeltaAmount, points))
		if err != nil {
			return err
		}
		settlement.PointLogID = &log.ID
	} else if settlement.SettlementType == "member_commission" {
		log, err := s.addPoints(ctx, settlement.UserID, points, "referral_reward", order.ID, order.OutOrderNo, fmt.Sprintf("member_commission order %s settled: %d cents -> %d points", settlement.OrderNo, settlement.DeltaAmount, points))
		if err != nil {
			return err
		}
		settlement.PointLogID = &log.ID
		s.safeNotify(ctx, settlement.UserID, "promotion", "推广奖励已到账", fmt.Sprintf("您的推广奖励 %d 积分已发放到账，可在推广中心查看。", points), "/promotion", "success")
	} else {
		log, err := s.addPoints(ctx, settlement.UserID, points, "activity_gift", order.ID, order.OutOrderNo, fmt.Sprintf("%s order %s settled: %d cents -> %d points", settlement.SettlementType, settlement.OrderNo, settlement.DeltaAmount, points))
		if err != nil {
			return err
		}
		settlement.PointLogID = &log.ID
	}
	settlement.PointsAmount = points
	settlement.Status = "settled"
	settlement.SettledAt = &now
	remark := fmt.Sprintf("已发放 %d 积分", points)
	if settlement.SettlementType == "agent_profit" {
		remark = fmt.Sprintf("已结算代理收益 %d 分并发放 %d 积分", settlement.DeltaAmount, points)
	}
	settlement.Remark = &remark
	return s.db.WithContext(ctx).Save(settlement).Error
}

func (s *Service) chargeAgentSubsidy(ctx context.Context, order *Order, settlement *AgentSettlement, now time.Time) error {
	amount := absInt(settlement.DeltaAmount)
	var agent UserAgent
	err := s.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("user_id = ?", settlement.UserID).
		First(&agent).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return BadRequest("代理身份不存在，无法扣除补贴")
	}
	if err != nil {
		return err
	}
	if agent.AgentFrozenBalance < amount {
		return BadRequest("代理冻结余额不足，无法扣除补贴")
	}
	frozenAfter := agent.AgentFrozenBalance - amount
	if err := s.db.WithContext(ctx).Model(&UserAgent{}).Where("id = ?", agent.ID).Update("agent_frozen_balance", frozenAfter).Error; err != nil {
		return err
	}
	remark := fmt.Sprintf("订单 %s 扣除代理补贴 %d 分", order.OutOrderNo, amount)
	if err := s.db.WithContext(ctx).Create(&AgentBalanceLog{
		UserID:             settlement.UserID,
		ChangeType:         "subsidy_charge",
		Delta:              -amount,
		BalanceAfter:       agent.AgentBalance,
		FrozenBalanceAfter: frozenAfter,
		OrderID:            &order.ID,
		RefNo:              &order.OutOrderNo,
		Remark:             &remark,
	}).Error; err != nil {
		return err
	}
	settlement.Status = "settled"
	settlement.SettledAt = &now
	settlement.Remark = &remark
	return s.db.WithContext(ctx).Save(settlement).Error
}

func (s *Service) creditAgentProfit(ctx context.Context, order *Order, settlement *AgentSettlement) error {
	amount := settlement.DeltaAmount
	var agent UserAgent
	err := s.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("user_id = ?", settlement.UserID).
		First(&agent).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return BadRequest("代理身份不存在，无法结算收益")
	}
	if err != nil {
		return err
	}
	balanceAfter := agent.AgentBalance + amount
	if err := s.db.WithContext(ctx).Model(&UserAgent{}).Where("id = ?", agent.ID).Update("agent_balance", balanceAfter).Error; err != nil {
		return err
	}
	remark := fmt.Sprintf("订单 %s 结算代理收益 %d 分", order.OutOrderNo, amount)
	return s.db.WithContext(ctx).Create(&AgentBalanceLog{
		UserID:             settlement.UserID,
		ChangeType:         "profit_settle",
		Delta:              amount,
		BalanceAfter:       balanceAfter,
		FrozenBalanceAfter: agent.AgentFrozenBalance,
		OrderID:            &order.ID,
		RefNo:              &order.OutOrderNo,
		Remark:             &remark,
	}).Error
}

func (s *Service) addPoints(ctx context.Context, userID int64, delta int, changeType string, orderID int64, refNo string, remark string) (PointLog, error) {
	if delta == 0 {
		return PointLog{}, nil
	}
	changeType = normalizedPointChangeType(changeType, delta)
	var log PointLog
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := s.withDB(tx)
		membership, err := txService.getOrCreateMembership(ctx, userID)
		if err != nil {
			return err
		}
		err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("id = ?", membership.ID).
			First(&membership).Error
		if err != nil {
			return err
		}
		balanceAfter := membership.AvailablePoints + delta
		if balanceAfter < 0 {
			return BadRequest(fmt.Sprintf("积分不足，当前可用 %d，仍需 %d", membership.AvailablePoints, -delta))
		}
		updates := map[string]any{"available_points": balanceAfter}
		if delta > 0 {
			updates["total_points"] = membership.TotalPoints + delta
		}
		if err := tx.Model(&UserMembership{}).Where("id = ?", membership.ID).Updates(updates).Error; err != nil {
			return err
		}
		var orderIDPtr *int64
		if orderID > 0 {
			orderIDPtr = &orderID
		}
		log = PointLog{
			UserID:       userID,
			ChangeType:   changeType,
			Delta:        delta,
			BalanceAfter: balanceAfter,
			OrderID:      orderIDPtr,
			RefNo:        &refNo,
			Remark:       &remark,
		}
		return tx.Create(&log).Error
	})
	return log, err
}

func normalizedPointChangeType(changeType string, delta int) string {
	switch changeType {
	case "order_reward", "order_refund", "points_use", "points_refund", "admin_adjust", "activity_gift", "agent_profit", "agent_subsidy", "referral_reward":
		return changeType
	case "self_purchase_reward":
		return "order_reward"
	}
	if delta < 0 {
		return "points_use"
	}
	return "activity_gift"
}

func (s *Service) safeNotify(ctx context.Context, userID int64, category string, title string, content string, link string, level string) {
	enabled, err := s.notificationInAppEnabled(ctx, userID, category)
	if err != nil || !enabled {
		return
	}
	senderName := notificationSenderName(category)
	notification := Notification{
		UserID:     userID,
		Category:   category,
		Level:      level,
		SenderName: &senderName,
		SenderType: category,
		Title:      title,
		Content:    content,
		Link:       &link,
		ActionURL:  &link,
		IsRead:     false,
	}
	_ = s.db.WithContext(ctx).Create(&notification).Error
}

func (s *Service) notificationInAppEnabled(ctx context.Context, userID int64, category string) (bool, error) {
	var pref NotificationPreference
	err := s.db.WithContext(ctx).Where("user_id = ? AND category = ?", userID, category).First(&pref).Error
	if err == nil {
		return pref.InAppEnabled, nil
	}
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return true, nil
	}
	return false, err
}

type targetSelection struct {
	coupon  *CouponApplicationResponse
	options []CouponTargetOption
}

func (s *Service) productCouponTargetSelection(ctx context.Context, userID int64, checkout checkoutContext, code *string) (*targetSelection, error) {
	coupon, err := s.validateCoupon(ctx, userID, checkout, code)
	if err != nil || coupon == nil {
		return nil, err
	}
	if coupon.DiscountLevel != "product" {
		return nil, nil
	}
	matched, err := s.matchedItemIDs(ctx, *coupon, checkout)
	if err != nil {
		return nil, err
	}
	if len(matched) == 0 {
		return nil, BadRequest("当前购物车中没有可使用该优惠券的商品")
	}
	options := []CouponTargetOption{}
	for _, item := range checkout.cartItems {
		if !matched[item.ID] {
			continue
		}
		itemTotal := maxInt(0, checkout.itemTotals[item.ID])
		discount := minInt(itemTotal, discountAmount(*coupon, itemTotal))
		if discount <= 0 {
			continue
		}
		product := checkout.products[item.ProductID]
		sku := product.SKU
		options = append(options, CouponTargetOption{
			CartItemID:     item.ID,
			ProductID:      item.ProductID,
			ProductName:    product.Name,
			SKU:            &sku,
			Cycle:          item.Cycle,
			Quantity:       item.Quantity,
			ItemTotal:      itemTotal,
			DiscountAmount: discount,
			FinalAmount:    maxInt(0, itemTotal-discount),
		})
	}
	if len(options) == 0 {
		return nil, BadRequest("当前购物车中没有可使用该优惠券的商品")
	}
	eligible := 0
	for _, option := range options {
		eligible += option.ItemTotal
	}
	summary := couponSummary(*coupon, checkout.total, eligible)
	return &targetSelection{coupon: &summary, options: options}, nil
}

func (s *Service) calculateCartCoupon(ctx context.Context, userID int64, checkout checkoutContext, code *string, targetCartItemID *int64) (*couponApplication, error) {
	coupon, err := s.validateCoupon(ctx, userID, checkout, code)
	if err != nil || coupon == nil {
		return nil, err
	}
	matched, err := s.matchedItemIDs(ctx, *coupon, checkout)
	if err != nil {
		return nil, err
	}
	if len(matched) == 0 {
		return nil, BadRequest("当前购物车中没有可使用该优惠券的商品")
	}

	discountLevel := defaultString(coupon.DiscountLevel, "order")
	itemDiscounts := map[int64]int{}
	targetProductID := (*int64)(nil)
	eligible := 0
	discount := 0
	if discountLevel == "product" {
		if targetCartItemID == nil {
			return nil, BadRequest("请先选择要使用产品级优惠券的商品")
		}
		if _, ok := checkout.itemTotals[*targetCartItemID]; !ok {
			return nil, BadRequest("所选商品不在当前购物车中")
		}
		if !matched[*targetCartItemID] {
			return nil, BadRequest("所选商品不符合该优惠券的使用范围")
		}
		eligible = maxInt(0, checkout.itemTotals[*targetCartItemID])
		for _, item := range checkout.cartItems {
			if item.ID == *targetCartItemID {
				value := item.ProductID
				targetProductID = &value
				break
			}
		}
		discount = minInt(eligible, discountAmount(*coupon, eligible))
		if discount > 0 {
			itemDiscounts[*targetCartItemID] = discount
		}
	} else if discountLevel == "order" {
		eligible = checkout.total
		discount = minInt(eligible, discountAmount(*coupon, eligible))
		itemDiscounts = orderItemDiscounts(checkout.itemTotals, discount)
	} else {
		return nil, BadRequest("优惠券层级不支持")
	}
	if discount <= 0 {
		return nil, BadRequest("该优惠券未产生可用优惠")
	}
	return &couponApplication{
		coupon:           *coupon,
		orderAmount:      checkout.total,
		eligibleAmount:   eligible,
		discountAmount:   discount,
		targetCartItemID: targetCartItemID,
		targetProductID:  targetProductID,
		itemDiscounts:    itemDiscounts,
	}, nil
}

func (s *Service) validateCoupon(ctx context.Context, userID int64, checkout checkoutContext, code *string) (*PromoCode, error) {
	normalized := normalizeCouponCode(code)
	if normalized == "" {
		return nil, nil
	}
	var coupon PromoCode
	err := s.db.WithContext(ctx).Where("UPPER(code) = ?", normalized).First(&coupon).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, NotFound("优惠券不存在")
	}
	if err != nil {
		return nil, err
	}
	if !coupon.IsEnabled {
		return nil, BadRequest("优惠券已停用")
	}
	now := time.Now().UTC()
	if coupon.StartsAt != nil && coupon.StartsAt.After(now) {
		return nil, BadRequest("优惠券尚未开始")
	}
	if coupon.ExpiresAt != nil && coupon.ExpiresAt.Before(now) {
		return nil, BadRequest("优惠券已过期")
	}
	if active, err := s.isActiveAgent(ctx, userID); err != nil {
		return nil, err
	} else if active {
		return nil, BadRequest("代理身份用户不可使用优惠券")
	}
	if coupon.MaxUses > 0 && coupon.UsedCount >= coupon.MaxUses {
		return nil, BadRequest("优惠券已被领用完")
	}
	if coupon.MaxUsesPerUser > 0 {
		used, err := s.userCouponUsedCount(ctx, coupon.ID, userID)
		if err != nil {
			return nil, err
		}
		if used >= coupon.MaxUsesPerUser {
			return nil, BadRequest("该账号已达到优惠券使用次数上限")
		}
	}
	if coupon.NewUsersOnly {
		hasPrior, err := s.hasPriorPaidOrder(ctx, userID)
		if err != nil {
			return nil, err
		}
		if hasPrior {
			return nil, BadRequest("该优惠券仅限新用户首单使用")
		}
	}
	if err := s.validateAgentCouponOwner(ctx, coupon, userID); err != nil {
		return nil, err
	}
	if err := s.validateMallClaim(ctx, coupon, userID); err != nil {
		return nil, err
	}
	if err := s.validateAdvancedRules(ctx, coupon, userID, checkout); err != nil {
		return nil, err
	}
	if coupon.MinOrderAmount > 0 && checkout.total < coupon.MinOrderAmount {
		return nil, BadRequest(fmt.Sprintf("订单金额需满 ¥%.2f 才可使用该优惠券", float64(coupon.MinOrderAmount)/100))
	}
	return &coupon, nil
}

func (s *Service) validateAdvancedRules(ctx context.Context, coupon PromoCode, userID int64, checkout checkoutContext) error {
	if ids := parseIntSet(coupon.AllowedUserIDs); len(ids) > 0 && !ids[userID] {
		return BadRequest("该优惠券仅限指定用户使用")
	}
	totalQuantity := 0
	for _, item := range checkout.cartItems {
		totalQuantity += maxInt(0, item.Quantity)
	}
	if coupon.MinQuantity > 0 && totalQuantity < coupon.MinQuantity {
		return BadRequest(fmt.Sprintf("订单商品数量需满 %d 件才可使用该优惠券", coupon.MinQuantity))
	}
	if ids := parseIntSet(coupon.RequiredProductIDs); len(ids) > 0 {
		found := false
		for _, item := range checkout.cartItems {
			if ids[item.ProductID] {
				found = true
				break
			}
		}
		if !found {
			return BadRequest("订单需包含指定产品才可使用该优惠券")
		}
	}
	if ids := parseIntSet(coupon.RequiredGroupIDs); len(ids) > 0 {
		found := false
		for _, product := range checkout.products {
			if ids[product.GroupID] {
				found = true
				break
			}
		}
		if !found {
			return BadRequest("订单需包含指定产品组才可使用该优惠券")
		}
	}
	if ids := parseIntSet(coupon.RequiredCategoryIDs); len(ids) > 0 {
		groupCategories, err := s.categoryIDsForProducts(ctx, checkout.products)
		if err != nil {
			return err
		}
		found := false
		for _, product := range checkout.products {
			if categoryID, ok := groupCategories[product.GroupID]; ok && ids[categoryID] {
				found = true
				break
			}
		}
		if !found {
			return BadRequest("订单需包含指定分类产品才可使用该优惠券")
		}
	}
	return nil
}

func (s *Service) matchedItemIDs(ctx context.Context, coupon PromoCode, checkout checkoutContext) (map[int64]bool, error) {
	cycles := parseStringSet(coupon.ApplicableCycles)
	scope := coupon.Scope
	if scope == "" {
		scope = "all"
	}
	scopeIDs := parseIntSet(coupon.ScopeIDs)
	groupCategories := map[int64]int64{}
	var err error
	if scope == "category" {
		groupCategories, err = s.categoryIDsForProducts(ctx, checkout.products)
		if err != nil {
			return nil, err
		}
	}
	matched := map[int64]bool{}
	for _, item := range checkout.cartItems {
		product, ok := checkout.products[item.ProductID]
		if !ok {
			continue
		}
		if len(cycles) > 0 && !cycles[strings.ToLower(item.Cycle)] {
			continue
		}
		itemMatches := scope == "all"
		switch scope {
		case "product":
			itemMatches = scopeIDs[product.ID]
		case "group":
			itemMatches = scopeIDs[product.GroupID]
		case "category":
			itemMatches = scopeIDs[groupCategories[product.GroupID]]
		}
		if itemMatches {
			matched[item.ID] = true
		}
	}
	return matched, nil
}

func (s *Service) categoryIDsForProducts(ctx context.Context, products map[int64]Product) (map[int64]int64, error) {
	groupIDs := []int64{}
	for _, product := range products {
		if product.GroupID > 0 {
			groupIDs = append(groupIDs, product.GroupID)
		}
	}
	if len(groupIDs) == 0 {
		return map[int64]int64{}, nil
	}
	var groups []ProductGroup
	if err := s.db.WithContext(ctx).Where("id IN ?", uniqueInt64(groupIDs)).Find(&groups).Error; err != nil {
		return nil, err
	}
	out := map[int64]int64{}
	for _, group := range groups {
		out[group.ID] = group.CategoryID
	}
	return out, nil
}

func (s *Service) calculatePointsDiscount(ctx context.Context, userID int64, orderAmount int, requestedPoints int) (int, int, error) {
	requestedPoints = maxInt(0, requestedPoints)
	requestedPoints = (requestedPoints / pointsToCentsRate) * pointsToCentsRate
	if requestedPoints <= 0 || orderAmount <= 0 {
		return 0, 0, nil
	}
	var membership UserMembership
	err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&membership).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return 0, 0, nil
	}
	if err != nil {
		return 0, 0, err
	}
	maxRate := 3500
	if membership.MemberLevelID != nil {
		var level MemberLevel
		err := s.db.WithContext(ctx).Where("id = ?", *membership.MemberLevelID).First(&level).Error
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, 0, err
		}
		if err == nil && level.MaxPointsDiscountRate > 0 {
			maxRate = level.MaxPointsDiscountRate
		}
	}
	actualPoints, pointsDiscount := calculatePointsDiscount(membership.AvailablePoints, orderAmount, maxRate)
	if requestedPoints < actualPoints {
		actualPoints = requestedPoints
		pointsDiscount = minInt(orderAmount, (actualPoints/pointsToCentsRate)*100)
	}
	return actualPoints, pointsDiscount, nil
}

func (s *Service) isActiveAgent(ctx context.Context, userID int64) (bool, error) {
	var count int64
	err := s.db.WithContext(ctx).
		Model(&UserAgent{}).
		Where("user_id = ? AND status IN ?", userID, []string{"active", "observing"}).
		Count(&count).Error
	return count > 0, err
}

func (s *Service) userCouponUsedCount(ctx context.Context, couponID int64, userID int64) (int, error) {
	var count int64
	err := s.db.WithContext(ctx).
		Model(&PromoCodeUsage{}).
		Where("promo_code_id = ? AND user_id = ? AND status = ?", couponID, userID, "used").
		Count(&count).Error
	return int(count), err
}

func (s *Service) hasPriorPaidOrder(ctx context.Context, userID int64) (bool, error) {
	var count int64
	err := s.db.WithContext(ctx).
		Model(&Order{}).
		Where("user_id = ? AND status IN ?", userID, []string{"paid", "refunded"}).
		Count(&count).Error
	return count > 0, err
}

func (s *Service) validateAgentCouponOwner(ctx context.Context, coupon PromoCode, userID int64) error {
	if coupon.IssuerType == nil || *coupon.IssuerType != "agent" {
		return nil
	}
	if coupon.IssuerUserID == nil {
		return BadRequest("代理优惠券归属异常，暂不可使用")
	}
	if *coupon.IssuerUserID == userID {
		return BadRequest("不能使用自己创建的代理优惠券")
	}
	active, err := s.isActiveAgent(ctx, *coupon.IssuerUserID)
	if err != nil {
		return err
	}
	if !active {
		return BadRequest("该代理优惠券所属代理未激活，暂不可使用")
	}
	var user UserAccount
	err = s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	if err == nil && user.BoundAgentID != nil && *user.BoundAgentID != *coupon.IssuerUserID {
		return BadRequest("该优惠券不属于您绑定的代理")
	}
	return nil
}

func (s *Service) validateMallClaim(ctx context.Context, coupon PromoCode, userID int64) error {
	if coupon.ClaimMethod != "mall" {
		return nil
	}
	var count int64
	err := s.db.WithContext(ctx).
		Model(&CouponMallClaim{}).
		Where("promo_code_id = ? AND user_id = ? AND status = ?", coupon.ID, userID, "claimed").
		Count(&count).Error
	if err != nil {
		return err
	}
	if count == 0 {
		return BadRequest("该优惠券需先在积分商城领取")
	}
	return nil
}

func newAvailableCouponResponse(coupon PromoCode) AvailableCouponResponse {
	var expires *string
	if coupon.ExpiresAt != nil {
		value := coupon.ExpiresAt.Format(time.RFC3339)
		expires = &value
	}
	return AvailableCouponResponse{
		ID:                coupon.ID,
		Description:       coupon.Description,
		Type:              coupon.Type,
		Value:             coupon.Value,
		ThresholdAmount:   coupon.MinOrderAmount,
		MaxDiscountAmount: coupon.MaxDiscountAmount,
		DiscountLevel:     defaultString(coupon.DiscountLevel, "order"),
		Scope:             defaultString(coupon.Scope, "all"),
		ExpiresAt:         expires,
	}
}

func newOrderListItemResponse(order Order) OrderListItemResponse {
	return OrderListItemResponse{
		ID:             order.ID,
		OutOrderNo:     order.OutOrderNo,
		Status:         order.Status,
		TotalAmount:    order.TotalAmount,
		DiscountAmount: order.DiscountAmount,
		CreatedAt:      order.CreatedAt,
	}
}

func newOrderDetailResponse(order Order, instances []Instance) OrderDetailResponse {
	out := OrderDetailResponse{
		ID:             order.ID,
		OutOrderNo:     order.OutOrderNo,
		Status:         order.Status,
		TotalAmount:    order.TotalAmount,
		DiscountAmount: order.DiscountAmount,
		ItemsSnapshot:  jsonList(order.ItemsSnapshot),
		CreatedAt:      order.CreatedAt,
		Instances:      make([]OrderInstanceResponse, 0, len(instances)),
	}
	for _, instance := range instances {
		out.Instances = append(out.Instances, OrderInstanceResponse{
			ID:        instance.ID,
			Status:    instance.Status,
			MainIP:    instance.MainIP,
			Hostname:  instance.Hostname,
			ExpiresAt: instance.ExpiresAt,
		})
	}
	return out
}

func couponSummary(coupon PromoCode, orderAmount int, eligibleAmount int) CouponApplicationResponse {
	return CouponApplicationResponse{
		Description:       coupon.Description,
		IssuerType:        coupon.IssuerType,
		IssuerUserID:      coupon.IssuerUserID,
		Type:              coupon.Type,
		Value:             coupon.Value,
		DiscountLevel:     defaultString(coupon.DiscountLevel, "order"),
		TargetCartItemID:  nil,
		TargetProductID:   nil,
		OrderAmount:       orderAmount,
		EligibleAmount:    eligibleAmount,
		DiscountAmount:    0,
		ItemDiscounts:     map[int64]int{},
		MaxDiscountAmount: coupon.MaxDiscountAmount,
		MinQuantity:       coupon.MinQuantity,
	}
}

func newCouponApplicationResponse(application couponApplication, finalAmount *int) CouponApplicationResponse {
	return CouponApplicationResponse{
		Description:       application.coupon.Description,
		IssuerType:        application.coupon.IssuerType,
		IssuerUserID:      application.coupon.IssuerUserID,
		Type:              application.coupon.Type,
		Value:             application.coupon.Value,
		DiscountLevel:     defaultString(application.coupon.DiscountLevel, "order"),
		TargetCartItemID:  application.targetCartItemID,
		TargetProductID:   application.targetProductID,
		OrderAmount:       application.orderAmount,
		EligibleAmount:    application.eligibleAmount,
		DiscountAmount:    application.discountAmount,
		ItemDiscounts:     application.itemDiscounts,
		MaxDiscountAmount: application.coupon.MaxDiscountAmount,
		MinQuantity:       application.coupon.MinQuantity,
		FinalAmount:       finalAmount,
	}
}

func normalizeCouponCode(code *string) string {
	if code == nil {
		return ""
	}
	return strings.ToUpper(strings.TrimSpace(*code))
}

func discountAmount(coupon PromoCode, eligibleAmount int) int {
	value := coupon.Value
	discount := 0
	switch coupon.Type {
	case "percent":
		percent := minInt(100, maxInt(0, value))
		discount = eligibleAmount * percent / 100
	case "fixed":
		discount = minInt(eligibleAmount, maxInt(0, value))
	case "threshold_fixed":
		if coupon.MinOrderAmount > 0 && eligibleAmount < coupon.MinOrderAmount {
			discount = 0
		} else {
			discount = minInt(eligibleAmount, maxInt(0, value))
		}
	case "override":
		discount = maxInt(0, eligibleAmount-maxInt(0, value))
	default:
		return 0
	}
	if coupon.MaxDiscountAmount > 0 {
		discount = minInt(discount, coupon.MaxDiscountAmount)
	}
	return discount
}

func orderItemDiscounts(itemTotals map[int64]int, discount int) map[int64]int {
	ids := make([]int64, 0, len(itemTotals))
	total := 0
	for id, amount := range itemTotals {
		if amount > 0 {
			ids = append(ids, id)
			total += amount
		}
	}
	sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
	out := map[int64]int{}
	if total <= 0 || discount <= 0 {
		return out
	}
	allocated := 0
	for _, id := range ids {
		amount := itemTotals[id] * discount / total
		out[id] = amount
		allocated += amount
	}
	remainder := discount - allocated
	for _, id := range ids {
		if remainder <= 0 {
			break
		}
		out[id]++
		remainder--
	}
	for id, amount := range out {
		if amount <= 0 {
			delete(out, id)
		}
	}
	return out
}

func applyCouponToTotals(itemTotals map[int64]int, application *couponApplication) int {
	payable := 0
	discounts := map[int64]int{}
	if application != nil {
		discounts = application.itemDiscounts
	}
	for id, total := range itemTotals {
		discount := minInt(maxInt(0, total), maxInt(0, discounts[id]))
		payable += maxInt(0, total-discount)
	}
	return payable
}

func calculatePointsDiscount(availablePoints int, orderAmount int, maxRate int) (int, int) {
	availablePoints = maxInt(0, availablePoints)
	maxDiscountCents := orderAmount * maxRate / 10000
	maxPointsDiscountCents := (availablePoints / pointsToCentsRate) * 100
	actualDiscountCents := minInt(maxDiscountCents, maxPointsDiscountCents)
	actualPoints := (actualDiscountCents / 100) * pointsToCentsRate
	return actualPoints, actualDiscountCents
}

func splitAmount(amount int, parts int) []int {
	if parts <= 0 {
		return []int{}
	}
	base := amount / parts
	remainder := amount % parts
	out := make([]int, parts)
	for idx := range out {
		out[idx] = base
		if idx < remainder {
			out[idx]++
		}
	}
	return out
}

func allocateFinalAmounts(amounts []int, finalAmount int, totalAmount int) []int {
	if len(amounts) == 0 {
		return []int{}
	}
	out := make([]int, len(amounts))
	if totalAmount <= 0 {
		return out
	}
	allocated := 0
	for idx, amount := range amounts {
		value := amount * finalAmount / totalAmount
		out[idx] = value
		allocated += value
	}
	remainder := finalAmount - allocated
	for idx := 0; idx < len(out) && remainder > 0; idx++ {
		out[idx]++
		remainder--
	}
	return out
}

func selfPurchaseReward(amountCents int, cashbackRate int) (int, int) {
	if amountCents <= 0 || cashbackRate <= 0 {
		return 0, 0
	}
	rewardCents := amountCents * cashbackRate / 10000
	return rewardCents * 10, rewardCents
}

func currentQuarterStart(now time.Time) time.Time {
	month := ((int(now.Month()) - 1) / 3 * 3) + 1
	return time.Date(now.Year(), time.Month(month), 1, 0, 0, 0, 0, now.Location())
}

func agentSettlementSnapshot(standardAmount int, customerPayAmount int, costRate int) agentSettlementCalc {
	costAmount := standardAmount * costRate / 10000
	delta := customerPayAmount - costAmount
	settlementType := "agent_profit"
	if delta < 0 {
		settlementType = "agent_subsidy"
	}
	return agentSettlementCalc{
		costRate:       costRate,
		costAmount:     costAmount,
		deltaAmount:    delta,
		pointsAmount:   maxInt(0, delta*10),
		settlementType: settlementType,
	}
}

func settlementReleaseAt(createdAt time.Time, freezeDays int) time.Time {
	if freezeDays <= 0 {
		freezeDays = defaultSettlementFreezeDays
	}
	if createdAt.IsZero() {
		createdAt = time.Now().UTC()
	}
	return createdAt.AddDate(0, 0, freezeDays)
}

func settingInt(raw JSONRaw, defaultValue int) int {
	if len(raw) == 0 {
		return defaultValue
	}
	var number int
	if err := json.Unmarshal(raw, &number); err == nil {
		return number
	}
	var text string
	if err := json.Unmarshal(raw, &text); err == nil {
		if parsed, err := strconv.Atoi(strings.TrimSpace(text)); err == nil {
			return parsed
		}
	}
	if parsed, err := strconv.Atoi(strings.TrimSpace(string(raw))); err == nil {
		return parsed
	}
	return defaultValue
}

func notificationSenderName(category string) string {
	switch category {
	case "promotion":
		return "推广中心"
	case "membership":
		return "会员中心"
	case "instance":
		return "云服务器中心"
	case "order":
		return "订单中心"
	default:
		return "系统通知"
	}
}

func parseStringSet(raw *string) map[string]bool {
	out := map[string]bool{}
	if raw == nil || strings.TrimSpace(*raw) == "" {
		return out
	}
	var values []any
	if err := json.Unmarshal([]byte(*raw), &values); err == nil {
		for _, value := range values {
			text := strings.ToLower(strings.TrimSpace(fmt.Sprint(value)))
			if text != "" {
				out[text] = true
			}
		}
		return out
	}
	for _, part := range strings.Split(*raw, ",") {
		text := strings.ToLower(strings.TrimSpace(part))
		if text != "" {
			out[text] = true
		}
	}
	return out
}

func parseIntSet(raw *string) map[int64]bool {
	out := map[int64]bool{}
	for value := range parseStringSet(raw) {
		var parsed int64
		if _, err := fmt.Sscan(value, &parsed); err == nil {
			out[parsed] = true
		}
	}
	return out
}

func uniqueInt64(values []int64) []int64 {
	seen := map[int64]struct{}{}
	out := make([]int64, 0, len(values))
	for _, value := range values {
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func yuanToCents(value float64) int {
	return int(math.Round(value * 100))
}

func jsonRawFromAny(value any) JSONRaw {
	if value == nil {
		return nil
	}
	raw, err := json.Marshal(value)
	if err != nil {
		return nil
	}
	return JSONRaw(raw)
}

func buildItemsSnapshot(checkout checkoutContext, application *couponApplication) []map[string]any {
	snapshots := make([]map[string]any, 0, len(checkout.cartItems))
	discounts := map[int64]int{}
	if application != nil {
		discounts = application.itemDiscounts
	}
	for _, item := range checkout.cartItems {
		product := checkout.products[item.ProductID]
		unit := yuanToCents(item.UnitPrice)
		addon := yuanToCents(item.AddonPrice)
		setup := yuanToCents(item.SetupFee)
		itemTotal := checkout.itemTotals[item.ID]
		couponDiscount := minInt(itemTotal, maxInt(0, discounts[item.ID]))
		payableBeforePoints := maxInt(0, itemTotal-couponDiscount)
		snapshot := map[string]any{
			"cart_item_id":                item.ID,
			"product_id":                  product.ID,
			"product_name":                product.Name,
			"sku":                         product.SKU,
			"cycle":                       item.Cycle,
			"quantity":                    item.Quantity,
			"unit_price_cents":            unit,
			"addon_price_cents":           addon,
			"setup_fee_cents":             setup,
			"item_total_cents":            itemTotal,
			"unit_price_yuan":             fmt.Sprintf("%.2f", float64(unit)/100),
			"os_image":                    item.OSImage,
			"alias":                       item.Alias,
			"slider_values":               jsonObject(item.SliderValues),
			"auto_renew":                  item.AutoRenew,
			"cloud_server_id":             product.CloudServerID,
			"api_type":                    product.APIType,
			"api_identifier":              product.APIIdentifier,
			"stock_policy":                product.StockPolicy,
			"stock_decremented":           product.StockPolicy == "tracked",
			"product_vcpu":                product.VCPU,
			"product_memory_mb":           parseMemoryMB(product.Memory),
			"provision_config":            jsonList(product.ProvisionConfig),
			"coupon_discount_cents":       couponDiscount,
			"payable_before_points_cents": payableBeforePoints,
		}
		if application != nil && couponDiscount > 0 {
			snapshot["coupon_code"] = application.coupon.Code
			snapshot["coupon_discount_level"] = defaultString(application.coupon.DiscountLevel, "order")
		}
		snapshots = append(snapshots, snapshot)
	}
	return snapshots
}

func generateOrderNo() string {
	return "ORD" + time.Now().UTC().Format("20060102150405") + randomDigits(6)
}

func randomDigits(n int) string {
	buf := make([]byte, n)
	if _, err := rand.Read(buf); err != nil {
		for i := range buf {
			buf[i] = byte(time.Now().UnixNano()>>uint(i%8)) % 10
		}
	}
	for i := range buf {
		buf[i] = '0' + (buf[i] % 10)
	}
	return string(buf)
}

func parseMemoryMB(value *string) *int {
	if value == nil {
		return nil
	}
	text := strings.TrimSpace(strings.ToLower(*value))
	if text == "" {
		return nil
	}
	numberPart := strings.Builder{}
	for _, r := range text {
		if (r >= '0' && r <= '9') || r == '.' {
			numberPart.WriteRune(r)
			continue
		}
		if numberPart.Len() > 0 {
			break
		}
	}
	if numberPart.Len() == 0 {
		return nil
	}
	number, err := strconv.ParseFloat(numberPart.String(), 64)
	if err != nil {
		return nil
	}
	mb := int(math.Round(number))
	if strings.Contains(text, "gb") || strings.Contains(text, "g") {
		mb = int(math.Round(number * 1024))
	}
	return &mb
}

func defaultString(value string, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func minInt(a int, b int) int {
	if a < b {
		return a
	}
	return b
}

func maxInt(a int, b int) int {
	if a > b {
		return a
	}
	return b
}

func absInt(value int) int {
	if value < 0 {
		return -value
	}
	return value
}
