// Package billing 时长包查询、下单与微信支付回调入账。
// PayClient 抽象真实微信支付（wechatpay-go，2B 商户号就绪后接入），开发期 MockPay。
// 设计见 doc/backend-architecture.html 3.3 与第五章。
package billing

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"dudu/server/internal/auth"
	"dudu/server/internal/quota"
	"dudu/server/internal/store"
	"dudu/server/pkg/protocol"
)

// NotifyResult 支付回调验签后的结果。
type NotifyResult struct {
	OrderID       string
	TransactionID string
	AmountCents   int
}

type PayClient interface {
	CreateNative(ctx context.Context, orderID string, amountCents int, desc string) (codeURL string, err error)
	CreateApp(ctx context.Context, orderID string, amountCents int, desc string) (params map[string]any, err error)
	// ParseNotify 验签并解析回调；签名不合法返回 error
	ParseNotify(r *http.Request) (NotifyResult, error)
	// Query 主动查单（兜底）：已支付返回 transactionID
	Query(ctx context.Context, orderID string) (paid bool, transactionID string, amountCents int, err error)
}

// MockPay 开发期实现：回调体为明文 JSON {order_id, transaction_id, amount_cents}。
type MockPay struct{}

func (MockPay) CreateNative(_ context.Context, orderID string, _ int, _ string) (string, error) {
	return "weixin://wxpay/mock/" + orderID, nil
}

func (MockPay) CreateApp(_ context.Context, orderID string, amountCents int, _ string) (map[string]any, error) {
	return map[string]any{"mock": true, "order_id": orderID, "amount": amountCents}, nil
}

func (MockPay) ParseNotify(r *http.Request) (NotifyResult, error) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		return NotifyResult{}, err
	}
	var p struct {
		OrderID       string `json:"order_id"`
		TransactionID string `json:"transaction_id"`
		AmountCents   int    `json:"amount_cents"`
	}
	if err := json.Unmarshal(body, &p); err != nil || p.OrderID == "" || p.TransactionID == "" {
		return NotifyResult{}, errors.New("invalid mock notify")
	}
	return NotifyResult{OrderID: p.OrderID, TransactionID: p.TransactionID, AmountCents: p.AmountCents}, nil
}

func (MockPay) Query(context.Context, string) (bool, string, int, error) { return false, "", 0, nil }

// ─── Handlers ────────────────────────────────────────────────────────────────

type Handlers struct {
	DB    *gorm.DB
	Pay   PayClient
	Quota *quota.Manager
}

// Packs GET /v1/packs 🔓
func (h *Handlers) Packs(c *gin.Context) {
	var rows []store.DurationPack
	if err := h.DB.Where("active").Order("sort").Find(&rows).Error; err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	resp := protocol.PacksResponse{Packs: make([]protocol.Pack, 0, len(rows))}
	for _, p := range rows {
		resp.Packs = append(resp.Packs, protocol.Pack{
			ID: p.ID, Minutes: p.Minutes, PriceCents: p.PriceCents, UnitDesc: p.UnitDesc, Tag: p.Tag,
		})
	}
	c.JSON(http.StatusOK, resp)
}

// CreateOrder POST /v1/orders（需登录）
func (h *Handlers) CreateOrder(c *gin.Context) {
	var req protocol.CreateOrderRequest
	if err := c.ShouldBindJSON(&req); err != nil ||
		(req.Channel != protocol.PayChannelNative && req.Channel != protocol.PayChannelApp) {
		c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
		return
	}
	var pack store.DurationPack
	if err := h.DB.Where("active").First(&pack, "id = ?", req.PackID).Error; err != nil {
		c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
		return
	}
	order := store.Order{
		ID:     "o" + strings.ReplaceAll(uuid.NewString(), "-", "")[:23],
		UserID: auth.UserID(c), PackID: pack.ID, PriceCents: pack.PriceCents,
		Channel: req.Channel, Status: store.OrderPending,
	}
	if err := h.DB.Create(&order).Error; err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	desc := "dudu 时长包 " + pack.ID
	resp := protocol.CreateOrderResponse{OrderID: order.ID}
	var err error
	if req.Channel == protocol.PayChannelNative {
		resp.CodeURL, err = h.Pay.CreateNative(c, order.ID, pack.PriceCents, desc)
	} else {
		resp.PayParams, err = h.Pay.CreateApp(c, order.ID, pack.PriceCents, desc)
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	c.JSON(http.StatusOK, resp)
}

// OrderStatus GET /v1/orders/:id（需登录，桌面轮询）
func (h *Handlers) OrderStatus(c *gin.Context) {
	var order store.Order
	if err := h.DB.First(&order, "id = ? AND user_id = ?", c.Param("id"), auth.UserID(c)).Error; err != nil {
		c.JSON(http.StatusNotFound, protocol.NewAPIError(protocol.ErrBadRequest))
		return
	}
	resp := protocol.OrderStatusResponse{Status: order.Status}
	if order.Status == store.OrderPaid {
		if snap, err := h.Quota.Get(c, order.UserID); err == nil {
			resp.BalanceSeconds = snap.BalanceSeconds
		}
	}
	c.JSON(http.StatusOK, resp)
}

// Notify POST /v1/pay/notify 🔓 微信支付回调：验签 → 幂等入账。
func (h *Handlers) Notify(c *gin.Context) {
	res, err := h.Pay.ParseNotify(c.Request)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"code": "FAIL", "message": "签名验证失败"})
		return
	}
	if err := h.MarkPaid(c, res); err != nil {
		slog.Error("mark paid failed", "err", err, "order", res.OrderID)
		c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "入账失败"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"code": "SUCCESS"})
}

// ErrAmountMismatch 金额与订单不符（拒绝入账并告警）。
var ErrAmountMismatch = errors.New("amount mismatch")

// MarkPaid 幂等入账：pending→paid + 时长 ledger + 余额冗余列，同一事务。
// 重复回调（订单已 paid 或 transaction_id 已存在）直接返回成功。
func (h *Handlers) MarkPaid(ctx context.Context, res NotifyResult) error {
	var credited bool
	var userID string
	var seconds int64
	err := h.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var order store.Order
		// GORM v2 行锁：v1 的 Set("gorm:query_option","FOR UPDATE") 在 v2 静默失效（16F）。
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			First(&order, "id = ?", res.OrderID).Error; err != nil {
			return err
		}
		if order.Status == store.OrderPaid {
			return nil // 幂等
		}
		if order.PriceCents != res.AmountCents {
			return ErrAmountMismatch
		}
		var pack store.DurationPack
		if err := tx.First(&pack, "id = ?", order.PackID).Error; err != nil {
			return err
		}
		now := time.Now()
		r := tx.Model(&store.Order{}).
			Where("id = ? AND status = ?", order.ID, store.OrderPending).
			Updates(map[string]any{"status": store.OrderPaid, "transaction_id": res.TransactionID, "paid_at": now})
		if r.Error != nil {
			return r.Error
		}
		if r.RowsAffected == 0 {
			return nil // 并发回调已处理
		}
		seconds = int64(pack.Minutes) * 60
		userID = order.UserID
		oid := order.ID
		if err := tx.Create(&store.BalanceLedger{
			UserID: userID, DeltaSeconds: seconds, Reason: store.LedgerPurchase, OrderID: &oid,
		}).Error; err != nil {
			return err
		}
		if err := tx.Model(&store.User{}).Where("id = ?", userID).
			UpdateColumn("balance_seconds", gorm.Expr("balance_seconds + ?", seconds)).Error; err != nil {
			return err
		}
		credited = true
		return nil
	})
	if err != nil {
		return err
	}
	if credited {
		// 失效 Redis 余额键 → 下次读取从 DB 重载（避免双写竞态）
		_ = h.Quota.InvalidateBalance(ctx, userID)
	}
	return nil
}

// StartWatchdog 查单兜底：每分钟扫描 pending 超 5 分钟的订单主动查单（防回调丢失）。
func (h *Handlers) StartWatchdog(ctx context.Context) {
	go func() {
		t := time.NewTicker(time.Minute)
		defer t.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-t.C:
				var orders []store.Order
				cutoff := time.Now().Add(-5 * time.Minute)
				if err := h.DB.Where("status = ? AND created_at < ?", store.OrderPending, cutoff).
					Limit(100).Find(&orders).Error; err != nil {
					continue
				}
				for _, o := range orders {
					paid, txn, amount, err := h.Pay.Query(ctx, o.ID)
					if err != nil || !paid {
						continue
					}
					if err := h.MarkPaid(ctx, NotifyResult{OrderID: o.ID, TransactionID: txn, AmountCents: amount}); err != nil {
						slog.Error("watchdog mark paid failed", "err", err, "order", o.ID)
					}
				}
			}
		}
	}()
}
