package handlers

import (
	"bytes"
	"context"
	"crypto/subtle"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"math"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"

	"mengyastore-backend/internal/auth"
	"mengyastore-backend/internal/database"
	"mengyastore-backend/internal/email"
	"mengyastore-backend/internal/models"
	"mengyastore-backend/internal/mq"
	"mengyastore-backend/internal/payment"
	"mengyastore-backend/internal/storage"
)

const qrSize = "320x320"

var errCheckoutProductInactive = errors.New("product inactive")

type checkoutBusyError struct{ wait int }

func (e *checkoutBusyError) Error() string {
	return fmt.Sprintf("正在有人购买中...请等待%d秒", e.wait)
}

type sameUserPendingError struct{ wait int }

func (e *sameUserPendingError) Error() string {
	return fmt.Sprintf("您有待支付的订单，请完成支付或等待%d秒后重试", e.wait)
}

type OrderHandler struct {
	productStore     *storage.ProductStore
	orderStore       *storage.OrderStore
	siteStore        *storage.SiteStore
	authClient       *auth.SproutGateClient
	mqClient         *mq.Client
	paymentPendingTTL time.Duration
	webhookMengyaSecret string
}

// CheckoutPayload 结账请求体（POST /api/checkout）。
type CheckoutPayload struct {
	ProductID     string `json:"productId"`
	Quantity      int    `json:"quantity"`
	Note          string `json:"note"`
	ContactPhone  string `json:"contactPhone"`
	ContactEmail  string `json:"contactEmail"`
	NotifyEmail   string `json:"notifyEmail"`
	PaymentMethod string `json:"paymentMethod"`
}

func NewOrderHandler(productStore *storage.ProductStore, orderStore *storage.OrderStore, siteStore *storage.SiteStore, authClient *auth.SproutGateClient, mqClient *mq.Client, pendingPaymentSecs int, webhookMengyaSecret string) *OrderHandler {
	secs := pendingPaymentSecs
	if secs <= 0 {
		secs = 60
	}
	return &OrderHandler{
		productStore:        productStore,
		orderStore:          orderStore,
		siteStore:           siteStore,
		authClient:          authClient,
		mqClient:            mqClient,
		paymentPendingTTL:   time.Duration(secs) * time.Second,
		webhookMengyaSecret: webhookMengyaSecret,
	}
}

func (h *OrderHandler) sendOrderNotify(toEmail, toName, productName, orderID string, qty int, codes []string, isManual bool) {
	if toEmail == "" {
		return
	}
	cfg, err := h.siteStore.GetSMTPConfig()
	if err != nil || !cfg.IsConfiguredEmail() {
		return
	}

	if h.mqClient != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		err := h.mqClient.PublishOrderEmail(ctx, mq.OrderEmailPayload{
			ToEmail:     toEmail,
			ToName:      toName,
			ProductName: productName,
			OrderID:     orderID,
			Quantity:    qty,
			Codes:       codes,
			IsManual:    isManual,
		})
		cancel()
		if err == nil {
			slog.Info("mq", "event", "order_email_queued", "order_id", orderID, "to_email", toEmail)
			return
		}
		slog.Warn("mq", "event", "publish_failed", "err", err)
	}

	go func() {
		emailCfg := email.Config{
			SMTPHost: cfg.Host,
			SMTPPort: cfg.Port,
			From:     cfg.Email,
			Password: cfg.Password,
			FromName: cfg.FromName,
		}
		if err := email.SendOrderNotify(emailCfg, email.OrderNotifyData{
			ToEmail:     toEmail,
			ToName:      toName,
			ProductName: productName,
			OrderID:     orderID,
			Quantity:    qty,
			Codes:       codes,
			IsManual:    isManual,
		}); err != nil {
			slog.Warn("email", "event", "order_notify_failed", "order_id", orderID, "to", toEmail, "err", err)
		} else {
			slog.Info("email", "event", "order_notify_sent", "order_id", orderID, "to", toEmail)
		}
	}()
}

func (h *OrderHandler) tryExtractUserWithEmail(c *gin.Context) (account, username, userEmail string) {
	authHeader := c.GetHeader("Authorization")
	if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
		return "", "", ""
	}
	userToken := strings.TrimPrefix(authHeader, "Bearer ")
	result, err := h.authClient.VerifyToken(userToken)
	if err != nil || !result.Valid || result.User == nil {
		return "", "", ""
	}
	return result.User.Account, result.User.Username, result.User.Email
}

func effectiveUnitPrice(p models.Product) float64 {
	if p.Price == 0 {
		return 0
	}
	if p.DiscountPrice > 0 && p.DiscountPrice < p.Price {
		return p.DiscountPrice
	}
	return p.Price
}

func snapMoney(v float64) float64 {
	return math.Round(v*100) / 100
}

func (h *OrderHandler) allocateInventory(product *models.Product, qty int) (delivered []string, updated models.Product, err error) {
	isFixed := strings.EqualFold(strings.TrimSpace(product.FulfillmentType), "fixed")
	if isFixed {
		content := strings.TrimSpace(product.FixedContent)
		if content == "" {
			return nil, models.Product{}, fmt.Errorf("fixed content missing")
		}
		delivered = make([]string, qty)
		for i := 0; i < qty; i++ {
			delivered[i] = content
		}
		up, e := h.productStore.GetByID(product.ID)
		return delivered, up, e
	}
	if product.Quantity < qty {
		return nil, models.Product{}, fmt.Errorf("out of stock")
	}
	deliveredCodes, ok := extractCodes(product, qty)
	if !ok {
		return nil, models.Product{}, fmt.Errorf("codes insufficient")
	}
	product.Quantity = len(product.Codes)
	up, e := h.productStore.Update(product.ID, *product)
	if e != nil {
		return nil, models.Product{}, e
	}
	return deliveredCodes, up, nil
}

func qrFromOrder(orderID, productID string) string {
	qrPayload := fmt.Sprintf("order:%s:%s", orderID, productID)
	return fmt.Sprintf("https://api.qrserver.com/v1/create-qr-code/?size=%s&data=%s", qrSize, url.QueryEscape(qrPayload))
}

// CreateOrder 创建订单（结账）。需登录与否取决于商品 requireLogin；Bearer 可选传入以关联账户与限购。
// @Summary 结账创建订单
// @Tags 订单
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer 用户 token（部分商品必填）"
// @Param body body CheckoutPayload true "结账参数"
// @Success 200 {object} SwaggerCheckoutWrap
// @Failure 400 {object} SwaggerErrorBody
// @Failure 401 {object} SwaggerErrorBody
// @Failure 404 {object} SwaggerErrorBody
// @Failure 409 {object} SwaggerErrorBody "库存锁定冲突或同用户待支付"
// @Failure 500 {object} SwaggerErrorBody
// @Router /api/checkout [post]
func (h *OrderHandler) CreateOrder(c *gin.Context) {
	userAccount, userName, userEmail := h.tryExtractUserWithEmail(c)

	var payload CheckoutPayload
	if err := c.ShouldBindJSON(&payload); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"})
		return
	}

	payload.ProductID = strings.TrimSpace(payload.ProductID)
	if payload.ProductID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "缺少必填字段"})
		return
	}
	if payload.Quantity <= 0 {
		payload.Quantity = 1
	}

	product, err := h.productStore.GetByID(payload.ProductID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
		return
	}
	if !product.Active {
		c.JSON(http.StatusBadRequest, gin.H{"error": "商品暂时无法购买"})
		return
	}
	if product.RequireLogin && userAccount == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "该商品需要登录后才能购买"})
		return
	}

	if product.MaxPerAccount > 0 && userAccount != "" {
		purchased, err := h.orderStore.CountPurchasedByAccount(userAccount, product.ID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if purchased+payload.Quantity > product.MaxPerAccount {
			remain := product.MaxPerAccount - purchased
			if remain <= 0 {
				c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("每个账户最多购买 %d 个，您已达上限", product.MaxPerAccount)})
			} else {
				c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("每个账户最多购买 %d 个，您还可购买 %d 个", product.MaxPerAccount, remain)})
			}
			return
		}
	}

	total := snapMoney(effectiveUnitPrice(product) * float64(payload.Quantity))

	rawPM := strings.ToLower(strings.TrimSpace(payload.PaymentMethod))
	if total > 0 && (rawPM == "legacy_manual" || rawPM == "legacy" || rawPM == "manual") {
		c.JSON(http.StatusBadRequest, gin.H{"error": "该支付方式已停用，请使用萌芽支付"})
		return
	}

	notifyEmail := strings.TrimSpace(userEmail)
	if notifyEmail == "" {
		notifyEmail = strings.TrimSpace(payload.NotifyEmail)
	}
	if notifyEmail == "" {
		notifyEmail = strings.TrimSpace(payload.ContactEmail)
	}

	deliveryMode := "auto"

	if total > 0 {
		hasQR := false
		for _, u := range product.PaymentQrURLs {
			if strings.TrimSpace(u) != "" {
				hasQR = true
				break
			}
		}
		if !hasQR {
			c.JSON(http.StatusBadRequest, gin.H{"error": "该商品尚未配置萌芽支付收款渠道，请在管理后台上传收款码图片链接"})
			return
		}
	}

	if total <= 0 {
		deliveredCodes, updatedProduct, err := h.allocateInventory(&product, payload.Quantity)
		if err != nil {
			msg := err.Error()
			switch msg {
			case "fixed content missing":
				c.JSON(http.StatusBadRequest, gin.H{"error": "该商品为固定内容发货，但未配置发货内容，请联系管理员"})
			case "out of stock":
				c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"})
			case "codes insufficient":
				c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"})
			default:
				c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
			}
			return
		}

		baseOrder := models.Order{
			ProductID:      updatedProduct.ID,
			ProductName:    updatedProduct.Name,
			UserAccount:    userAccount,
			UserName:       userName,
			Quantity:       payload.Quantity,
			DeliveredCodes: deliveredCodes,
			DeliveryMode:   deliveryMode,
			Note:           strings.TrimSpace(payload.Note),
			ContactPhone:   strings.TrimSpace(payload.ContactPhone),
			ContactEmail:   strings.TrimSpace(payload.ContactEmail),
			NotifyEmail:    notifyEmail,
		}

		baseOrder.Status = "completed"
		baseOrder.PaymentMethod = payment.MethodMengya
		created, err := h.orderStore.Create(baseOrder)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if err := h.productStore.IncrementSold(updatedProduct.ID, payload.Quantity); err != nil {
			slog.Warn("order", "event", "increment_sold_failed", "err", err)
		}
		h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, deliveredCodes, false)
		resp := gin.H{
			"orderId":         created.ID,
			"qrCodeUrl":       qrFromOrder(created.ID, created.ProductID),
			"productId":       created.ProductID,
			"productQty":      created.Quantity,
			"viewCount":       updatedProduct.ViewCount,
			"status":          created.Status,
			"paymentMethod":   payment.MethodMengya,
		}
		c.JSON(http.StatusOK, gin.H{"data": resp})
		return
	}

	var (
		created        models.Order
		updatedProduct models.Product
	)

	err = h.productStore.Transaction(func(tx *gorm.DB) error {
		now := time.Now().UTC()
		plock, err := h.productStore.GetByIDForUpdate(tx, payload.ProductID)
		if err != nil {
			return err
		}
		if !plock.Active {
			return errCheckoutProductInactive
		}

		var block database.OrderRow
		err = tx.Where("product_id = ? AND status = ? AND payment_expires_at IS NOT NULL AND payment_expires_at > ?",
			payload.ProductID, "pending_payment", now).
			Order("created_at ASC").First(&block).Error
		if err == nil {
			w := int(math.Ceil(block.PaymentExpiresAt.Sub(now).Seconds()))
			if w < 1 {
				w = 1
			}
			req := strings.TrimSpace(userAccount)
			holder := strings.TrimSpace(block.UserAccount)
			if req != "" && holder == req {
				return &sameUserPendingError{wait: w}
			}
			return &checkoutBusyError{wait: w}
		}
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}

		deliveredCodes, updated, err := h.allocateInventoryTx(tx, &plock, payload.Quantity)
		if err != nil {
			return err
		}
		updatedProduct = updated

		exp := time.Now().UTC().Add(h.paymentPendingTTL)
		pending := models.Order{
			ProductID:            updated.ID,
			ProductName:          updated.Name,
			UserAccount:          userAccount,
			UserName:             userName,
			Quantity:             payload.Quantity,
			DeliveredCodes:       deliveredCodes,
			DeliveryMode:         deliveryMode,
			Note:                 strings.TrimSpace(payload.Note),
			ContactPhone:         strings.TrimSpace(payload.ContactPhone),
			ContactEmail:         strings.TrimSpace(payload.ContactEmail),
			NotifyEmail:          notifyEmail,
			Status:               "pending_payment",
			PaymentMethod:        payment.MethodMengya,
			PaymentExpectedTotal: total,
			PaymentExpiresAt:     &exp,
		}
		cr, err := h.orderStore.CreateTx(tx, pending)
		if err != nil {
			return err
		}
		created = cr
		return nil
	})

	if err != nil {
		var busy *checkoutBusyError
		if errors.As(err, &busy) {
			c.JSON(http.StatusConflict, gin.H{"error": busy.Error()})
			return
		}
		var dup *sameUserPendingError
		if errors.As(err, &dup) {
			c.JSON(http.StatusConflict, gin.H{"error": dup.Error()})
			return
		}
		if errors.Is(err, errCheckoutProductInactive) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "商品暂时无法购买"})
			return
		}
		msg := err.Error()
		switch msg {
		case "fixed content missing":
			c.JSON(http.StatusBadRequest, gin.H{"error": "该商品为固定内容发货，但未配置发货内容，请联系管理员"})
		case "out of stock":
			c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"})
		case "codes insufficient":
			c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"})
		case "product not found":
			c.JSON(http.StatusNotFound, gin.H{"error": "商品不存在"})
		default:
			c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
		}
		return
	}

	resp := gin.H{
		"orderId":              created.ID,
		"qrCodeUrl":            qrFromOrder(created.ID, created.ProductID),
		"productId":            created.ProductID,
		"productQty":           created.Quantity,
		"viewCount":            updatedProduct.ViewCount,
		"status":               created.Status,
		"paymentMethod":        payment.MethodMengya,
		"paymentExpectedTotal": created.PaymentExpectedTotal,
		"paymentQrUrls":        updatedProduct.PaymentQrURLs,
		"paymentExpiresAt":     created.PaymentExpiresAt,
	}
	c.JSON(http.StatusOK, gin.H{"data": resp})
}

func confirmResponse(order models.Order) gin.H {
	isManual := order.DeliveryMode == "manual"
	return gin.H{
		"orderId":        order.ID,
		"status":         order.Status,
		"deliveryMode":   order.DeliveryMode,
		"deliveredCodes": order.DeliveredCodes,
		"isManual":       isManual,
	}
}

// ConfirmOrder 用户确认订单状态（手动核销、待支付轮询后取货等）。
// @Summary 确认订单
// @Tags 订单
// @Produce json
// @Param id path string true "订单 ID"
// @Success 200 {object} SwaggerConfirmWrap
// @Failure 404 {object} SwaggerErrorBody
// @Failure 409 {object} SwaggerErrorBody "待支付未到账等"
// @Failure 410 {object} SwaggerErrorBody "已取消或超时"
// @Router /api/orders/{id}/confirm [post]
func (h *OrderHandler) ConfirmOrder(c *gin.Context) {
	orderID := c.Param("id")
	order, err := h.orderStore.GetByID(orderID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
		return
	}

	switch order.Status {
	case "completed":
		isManual := order.DeliveryMode == "manual"
		if isManual {
			confirmNotifyEmail := order.NotifyEmail
			if confirmNotifyEmail == "" {
				confirmNotifyEmail = order.ContactEmail
			}
			h.sendOrderNotify(confirmNotifyEmail, order.UserName, order.ProductName, order.ID, order.Quantity, order.DeliveredCodes, false)
		}
		c.JSON(http.StatusOK, gin.H{"data": confirmResponse(order)})
		return

	case "cancelled":
		c.JSON(http.StatusGone, gin.H{"error": "订单已取消"})
		return

	case "pending_payment":
		h.syncExpiredPending(order)
		order, err = h.orderStore.GetByID(orderID)
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		if order.Status == "cancelled" {
			c.JSON(http.StatusGone, gin.H{"error": "订单已超时未支付"})
			return
		}
		if order.Status == "completed" {
			h.sendOrderNotify(order.NotifyEmail, order.UserName, order.ProductName, order.ID, order.Quantity, order.DeliveredCodes, false)
			c.JSON(http.StatusOK, gin.H{"data": confirmResponse(order)})
			return
		}
		c.JSON(http.StatusConflict, gin.H{"error": "等待到账确认，到账后系统将自动核销；您也可稍后再次点击"})
		return

	default:
		// 手动待发货等，状态仍为 pending
		order2, err := h.orderStore.Confirm(orderID)
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		isManual := order2.DeliveryMode == "manual"
		if isManual {
			confirmNotifyEmail := order2.NotifyEmail
			if confirmNotifyEmail == "" {
				confirmNotifyEmail = order2.ContactEmail
			}
			h.sendOrderNotify(confirmNotifyEmail, order2.UserName, order2.ProductName, order2.ID, order2.Quantity, order2.DeliveredCodes, false)
		}
		c.JSON(http.StatusOK, gin.H{"data": confirmResponse(order2)})
		return
	}
}

// GetOrderPaymentStatus 前端轮询订单支付状态（不返回卡密）。
// @Summary 订单支付状态
// @Tags 订单
// @Produce json
// @Param id path string true "订单 ID"
// @Success 200 {object} SwaggerPaymentStatusWrap
// @Failure 404 {object} SwaggerErrorBody
// @Router /api/orders/{id}/payment-status [get]
func (h *OrderHandler) GetOrderPaymentStatus(c *gin.Context) {
	orderID := c.Param("id")
	order, err := h.orderStore.GetByID(orderID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
		return
	}

	if order.Status == "pending_payment" {
		h.syncExpiredPending(order)
		order, _ = h.orderStore.GetByID(orderID)
	}

	out := gin.H{
		"status":           order.Status,
		"expectedTotal":    order.PaymentExpectedTotal,
		"paymentExpiresAt": order.PaymentExpiresAt,
		"paymentMethod":    order.PaymentMethod,
	}
	c.JSON(http.StatusOK, gin.H{"data": out})
}

func (h *OrderHandler) syncExpiredPending(order models.Order) {
	if order.Status != "pending_payment" || order.PaymentExpiresAt == nil {
		return
	}
	if time.Now().UTC().Before(*order.PaymentExpiresAt) {
		return
	}
	if err := h.releasePendingReserved(order); err != nil {
		slog.Warn("payment", "event", "expire_release_failed", "order_id", order.ID, "err", err)
	}
}

func (h *OrderHandler) releasePendingReserved(o models.Order) error {
	changed, err := h.orderStore.CancelPendingStaleIfEligible(o.ID)
	if err != nil {
		return err
	}
	if !changed {
		return nil
	}
	up, err := h.productStore.GetByID(o.ProductID)
	if err != nil {
		return err
	}
	fixed := strings.EqualFold(strings.TrimSpace(up.FulfillmentType), "fixed")
	if !fixed && len(o.DeliveredCodes) > 0 {
		if err := h.productStore.PrependReservedCodes(o.ProductID, o.DeliveredCodes); err != nil {
			return err
		}
	}
	return nil
}
func (h *OrderHandler) releaseAllExpired(now time.Time) {
	list, err := h.orderStore.ListExpiredPendingPayments(now)
	if err != nil {
		slog.Warn("payment", "event", "list_expired_failed", "err", err)
		return
	}
	for _, o := range list {
		if err := h.releasePendingReserved(o); err != nil {
			slog.Warn("payment", "event", "expire_sweep_failed", "order_id", o.ID, "err", err)
		}
	}
}

// RunExpiredPaymentSweep 后台定期释放超时未支付的预留库存。
func (h *OrderHandler) RunExpiredPaymentSweep(ctx context.Context, every time.Duration) {
	t := time.NewTicker(every)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			h.releaseAllExpired(time.Now().UTC())
		}
	}
}

func webhookSecretMatches(expected string, hdr string) bool {
	if expected == "" {
		return true
	}
	got := strings.TrimSpace(hdr)
	if len(got) != len(expected) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(expected), []byte(got)) == 1
}

func webhookLogVerbose() bool {
	switch strings.ToLower(strings.TrimSpace(os.Getenv("WEBHOOK_LOG_VERBOSE"))) {
	case "1", "true", "yes", "on":
		return true
	default:
		return false
	}
}

func clampOneLine(s string, maxRunes int) string {
	if maxRunes <= 0 {
		maxRunes = 200
	}
	s = strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", " "), "\n", " ")
	s = strings.TrimSpace(s)
	r := []rune(s)
	if len(r) > maxRunes {
		return string(r[:maxRunes]) + "…"
	}
	return s
}

// MengyaPaymentWebhook 萌芽支付到账通知（Body 为渠道 JSON）。若配置 WEBHOOK_MENGYA_SECRET 则必须带 X-Webhook-Secret。
// @Summary 萌芽支付 Webhook
// @Tags Webhook
// @Accept json
// @Produce json
// @Param X-Webhook-Secret header string false "与 WEBHOOK_MENGYA_SECRET 一致"
// @Success 200 {object} SwaggerWebhookMengyaResp
// @Failure 400 {object} SwaggerErrorBody
// @Failure 403 {object} SwaggerErrorBody
// @Failure 500 {object} SwaggerErrorBody
// @Router /api/webhooks/mengya-pay [post]
func (h *OrderHandler) MengyaPaymentWebhook(c *gin.Context) {
	verbose := webhookLogVerbose()
	if !webhookSecretMatches(h.webhookMengyaSecret, c.GetHeader("X-Webhook-Secret")) {
		slog.Warn("mengya webhook", "result", "forbidden", "remote", c.ClientIP())
		c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
		return
	}

	raw, err := io.ReadAll(c.Request.Body)
	if err != nil {
		slog.Warn("mengya webhook", "result", "read_body", "err", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "read body"})
		return
	}

	noticeHint := payment.NoticeFieldsForLog(raw)
	amount, parseOK := payment.ParseWebhookJSON(raw)

	base := []any{
		"remote", c.ClientIP(),
		"bytes", len(raw),
		"parse_ok", parseOK,
		"amount", amount,
	}
	if verbose {
		base = append(base,
			"notice", clampOneLine(noticeHint, 280),
			"body", clampOneLine(string(raw), 600),
		)
	}

	if !parseOK || amount <= 0 {
		if json.Valid(bytes.TrimSpace(raw)) && noticeHint == "" {
			base = append(base, "hint", "json_without_notice_keys")
		}
		slog.Warn("mengya webhook", append([]any{"result", "parse_amount_failed"}, base...)...)
		c.JSON(http.StatusBadRequest, gin.H{"error": "无法解析到账金额"})
		return
	}

	o, matched, err := h.orderStore.TryFulfillMengyaPayment(amount, time.Now().UTC())
	if err != nil {
		slog.Error("mengya webhook", append([]any{"result", "fulfill_error", "err", err}, base...)...)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"})
		return
	}

	orderID := "-"
	if matched {
		orderID = o.ID
	}
	result := "no_order_match"
	if matched {
		result = "ok"
	}
	slog.Info("mengya webhook", append([]any{"result", result, "matched", matched, "order_id", orderID}, base...)...)

	out := gin.H{"ok": true, "matched": matched}
	if matched {
		fulfilled, err := h.orderStore.GetByID(o.ID)
		if err == nil {
			h.sendOrderNotify(fulfilled.NotifyEmail, fulfilled.UserName, fulfilled.ProductName, fulfilled.ID, fulfilled.Quantity, fulfilled.DeliveredCodes, false)
			out["matched_order_id"] = fulfilled.ID
		}
	}
	c.JSON(http.StatusOK, out)
}

// CancelOrder 用户取消待支付订单并释放预留库存（当前实现不校验 Bearer，凭订单 ID 操作）。
// @Summary 取消订单
// @Tags 订单
// @Produce json
// @Param id path string true "订单 ID"
// @Success 200 {object} SwaggerCancelWrap
// @Failure 404 {object} SwaggerErrorBody
// @Failure 409 {object} SwaggerErrorBody
// @Failure 500 {object} SwaggerErrorBody
// @Router /api/orders/{id}/cancel [post]
func (h *OrderHandler) CancelOrder(c *gin.Context) {
	orderID := c.Param("id")
	order, err := h.orderStore.GetByID(orderID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "订单不存在"})
		return
	}
	switch order.Status {
	case "cancelled":
		c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "message": "订单已取消"}})
		return
	case "completed":
		c.JSON(http.StatusConflict, gin.H{"error": "已完成的订单无法取消"})
		return
	case "pending_payment":
		if err := h.releasePendingReserved(order); err != nil {
			slog.Warn("order", "event", "cancel_release_failed", "order_id", order.ID, "err", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "取消失败，请稍候再试"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "message": "订单已取消"}})
		return
	default:
		c.JSON(http.StatusConflict, gin.H{"error": "当前订单状态不支持取消"})
		return
	}
}

// ListMyOrders 当前登录用户的订单列表（待支付条目不返回卡密）。
// @Summary 我的订单
// @Tags 订单
// @Produce json
// @Security BearerAuth
// @Success 200 {object} SwaggerOrdersBody
// @Failure 401 {object} SwaggerErrorBody
// @Failure 500 {object} SwaggerErrorBody
// @Router /api/orders [get]
func (h *OrderHandler) ListMyOrders(c *gin.Context) {
	authHeader := c.GetHeader("Authorization")
	if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
		return
	}

	userToken := strings.TrimPrefix(authHeader, "Bearer ")
	result, err := h.authClient.VerifyToken(userToken)
	if err != nil || !result.Valid || result.User == nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期，请重新登录"})
		return
	}

	orders, err := h.orderStore.ListByAccount(result.User.Account)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	for i := range orders {
		if orders[i].Status == "pending_payment" {
			orders[i].DeliveredCodes = nil
		}
	}

	c.JSON(http.StatusOK, gin.H{"data": orders})
}

func (h *OrderHandler) allocateInventoryTx(tx *gorm.DB, product *models.Product, qty int) (delivered []string, updated models.Product, err error) {
	isFixed := strings.EqualFold(strings.TrimSpace(product.FulfillmentType), "fixed")
	if isFixed {
		content := strings.TrimSpace(product.FixedContent)
		if content == "" {
			return nil, models.Product{}, fmt.Errorf("fixed content missing")
		}
		delivered = make([]string, qty)
		for i := 0; i < qty; i++ {
			delivered[i] = content
		}
		up, e := h.productStore.GetByIDForUpdate(tx, product.ID)
		return delivered, up, e
	}
	if product.Quantity < qty {
		return nil, models.Product{}, fmt.Errorf("out of stock")
	}
	deliveredCodes, ok := extractCodes(product, qty)
	if !ok {
		return nil, models.Product{}, fmt.Errorf("codes insufficient")
	}
	product.Quantity = len(product.Codes)
	up, e := h.productStore.UpdateTx(tx, product.ID, *product)
	if e != nil {
		return nil, models.Product{}, e
	}
	return deliveredCodes, up, nil
}

func extractCodes(product *models.Product, count int) ([]string, bool) {
	if count <= 0 {
		return nil, false
	}
	if len(product.Codes) < count {
		return nil, false
	}
	delivered := make([]string, count)
	copy(delivered, product.Codes[:count])
	product.Codes = product.Codes[count:]
	return delivered, true
}
