// 积分账务模块，负责积分入账、扣减、退款及批次分配记录维护。
package billing

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/google/uuid"
	"gorm.io/gorm"
)

var ErrInsufficientPoints = errors.New("积分余额不足，请前往充值")

// pointGrant 表示参与扣减的可用积分批次。
type pointGrant struct {
	ID              uuid.UUID
	AvailableAmount string
}

// pointAllocation 表示原扣减流水关联的积分批次和分配金额。
type pointAllocation struct {
	GrantID uuid.UUID
	Amount  string
}

// parsePointCents 将最多两位小数的积分字符串转换为整数分值。
func parsePointCents(value string) (int64, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return 0, errors.New("积分数量为空")
	}
	parts := strings.Split(value, ".")
	if len(parts) > 2 || strings.HasPrefix(parts[0], "-") {
		return 0, fmt.Errorf("无效积分数量 %q", value)
	}
	whole, err := strconv.ParseInt(parts[0], 10, 64)
	if err != nil {
		return 0, fmt.Errorf("无效积分数量 %q", value)
	}
	fraction := ""
	if len(parts) == 2 {
		fraction = parts[1]
	}
	if len(fraction) > 2 {
		if strings.Trim(fraction[2:], "0") != "" {
			return 0, fmt.Errorf("积分数量最多保留两位小数")
		}
		fraction = fraction[:2]
	}
	fraction += strings.Repeat("0", 2-len(fraction))
	fractionValue, err := strconv.ParseInt(fraction, 10, 64)
	if err != nil || whole > (int64(^uint64(0)>>1)-fractionValue)/100 {
		return 0, fmt.Errorf("无效积分数量 %q", value)
	}
	return whole*100 + fractionValue, nil
}

// formatPointCents 将整数分值格式化为两位小数的积分字符串。
func formatPointCents(value int64) string {
	return fmt.Sprintf("%d.%02d", value/100, value%100)
}

// lockUser 锁定用户积分余额，避免并发账务操作导致余额和批次不一致。
func lockUser(tx *gorm.DB, userID uuid.UUID, requireActive bool) (string, error) {
	var balance string
	userQuery := "SELECT point_balance::text FROM web_users WHERE id=? FOR UPDATE"
	if requireActive {
		userQuery = "SELECT point_balance::text FROM web_users WHERE id=? AND enabled=true AND deleted_at IS NULL FOR UPDATE"
	}
	if err := tx.Raw(userQuery, userID).Scan(&balance).Error; err != nil {
		return "", err
	}
	if balance == "" {
		return "", gorm.ErrRecordNotFound
	}
	return balance, nil
}

// CreditPoints 创建永久有效的积分批次，并原子写入用户余额和积分流水。
func CreditPoints(tx *gorm.DB, userID uuid.UUID, sourceType, sourceID, points, remark string) (string, bool, error) {
	amount, err := parsePointCents(points)
	if err != nil || amount <= 0 {
		return "", false, errors.New("入账积分必须大于零")
	}
	balance, err := lockUser(tx, userID, true)
	if err != nil {
		return "", false, err
	}

	grantID := uuid.New()
	result := tx.Exec(`INSERT INTO point_grants(id,user_id,source_type,source_id,granted_amount,available_amount)
		VALUES(?,?,?,?,?::numeric,?::numeric) ON CONFLICT(source_type,source_id) DO NOTHING`,
		grantID, userID, sourceType, sourceID, points, points)
	if result.Error != nil {
		return "", false, result.Error
	}
	if result.RowsAffected == 0 {
		return balance, false, nil
	}
	if err := tx.Raw(`UPDATE web_users SET point_balance=point_balance+?::numeric
		WHERE id=? RETURNING point_balance::text`, points, userID).Scan(&balance).Error; err != nil {
		return "", false, err
	}
	if err := tx.Exec(`INSERT INTO point_ledger(user_id,change_amount,balance_after,business_type,business_id,remark,idempotency_key)
		VALUES(?,?::numeric,?::numeric,?,?,?,?)`, userID, points, balance, sourceType, sourceID, remark, sourceType+":"+sourceID).Error; err != nil {
		return "", false, err
	}
	return balance, true, nil
}

// CreditRedemptionPoints 将兑换码对应积分以永久有效批次计入用户账户。
func CreditRedemptionPoints(tx *gorm.DB, userID, codeID uuid.UUID, points string) (string, bool, error) {
	return CreditPoints(tx, userID, "redemption", codeID.String(), points, "兑换码兑换")
}

// DebitPoints 按批次创建顺序扣减积分，并记录分配明细供后续退款恢复。
func DebitPoints(tx *gorm.DB, userID uuid.UUID, points, businessType, businessID, remark, idempotencyKey string) (string, error) {
	requested, err := parsePointCents(points)
	if err != nil {
		return "", err
	}
	balance, err := lockUser(tx, userID, true)
	if err != nil {
		return "", err
	}
	if requested == 0 {
		return balance, nil
	}

	var grants []pointGrant
	if err := tx.Raw(`SELECT id,available_amount::text AS available_amount
		FROM point_grants WHERE user_id=? AND available_amount>0
		ORDER BY created_at,id FOR UPDATE`, userID).Scan(&grants).Error; err != nil {
		return "", err
	}
	remaining := requested
	type usedGrant struct {
		id     uuid.UUID
		amount int64
	}
	used := make([]usedGrant, 0, len(grants))
	for _, grant := range grants {
		available, parseErr := parsePointCents(grant.AvailableAmount)
		if parseErr != nil {
			return "", parseErr
		}
		use := available
		if use > remaining {
			use = remaining
		}
		if use > 0 {
			used = append(used, usedGrant{id: grant.ID, amount: use})
			remaining -= use
		}
		if remaining == 0 {
			break
		}
	}
	if remaining > 0 {
		return "", ErrInsufficientPoints
	}
	for _, allocation := range used {
		if err := tx.Exec(`UPDATE point_grants SET available_amount=available_amount-?::numeric WHERE id=?`,
			formatPointCents(allocation.amount), allocation.id).Error; err != nil {
			return "", err
		}
	}
	if err := tx.Raw(`UPDATE web_users SET point_balance=point_balance-?::numeric
		WHERE id=? RETURNING point_balance::text`, points, userID).Scan(&balance).Error; err != nil {
		return "", err
	}
	var ledgerID int64
	if err := tx.Raw(`INSERT INTO point_ledger(user_id,change_amount,balance_after,business_type,business_id,remark,idempotency_key)
		VALUES(?,(-?::numeric),?::numeric,?,?,?,?) RETURNING id`,
		userID, points, balance, businessType, businessID, remark, idempotencyKey).Scan(&ledgerID).Error; err != nil {
		return "", err
	}
	for _, allocation := range used {
		if err := tx.Exec(`INSERT INTO point_ledger_allocations(ledger_id,grant_id,allocation_type,amount)
			VALUES(?,?,'consume',?::numeric)`, ledgerID, allocation.id, formatPointCents(allocation.amount)).Error; err != nil {
			return "", err
		}
	}
	return balance, nil
}

// RefundDebit 将原扣减记录恢复到对应的永久积分批次，并写入退款流水。
func RefundDebit(tx *gorm.DB, userID uuid.UUID, originalKey, refundKey, businessType, businessID, remark string) (bool, error) {
	balance, err := lockUser(tx, userID, false)
	if err != nil {
		return false, err
	}
	var existing int64
	if err := tx.Raw("SELECT count(*) FROM point_ledger WHERE idempotency_key=?", refundKey).Scan(&existing).Error; err != nil {
		return false, err
	}
	if existing > 0 {
		return false, nil
	}

	var allocations []pointAllocation
	if err := tx.Raw(`SELECT a.grant_id,a.amount::text AS amount
		FROM point_ledger original
		JOIN point_ledger_allocations a ON a.ledger_id=original.id AND a.allocation_type='consume'
		JOIN point_grants g ON g.id=a.grant_id
		WHERE original.idempotency_key=? ORDER BY g.created_at,g.id FOR UPDATE OF g`, originalKey).Scan(&allocations).Error; err != nil {
		return false, err
	}
	if len(allocations) == 0 {
		return false, errors.New("未找到原积分扣减分配记录")
	}
	restored := int64(0)
	restoredAllocations := make([]pointAllocation, 0, len(allocations))
	for _, allocation := range allocations {
		amount, parseErr := parsePointCents(allocation.Amount)
		if parseErr != nil {
			return false, parseErr
		}
		result := tx.Exec(`UPDATE point_grants SET available_amount=available_amount+?::numeric
			WHERE id=?`, allocation.Amount, allocation.GrantID)
		if result.Error != nil {
			return false, result.Error
		}
		if result.RowsAffected > 0 {
			restored += amount
			restoredAllocations = append(restoredAllocations, allocation)
		}
	}
	if restored > 0 {
		if err := tx.Raw(`UPDATE web_users SET point_balance=point_balance+?::numeric
			WHERE id=? RETURNING point_balance::text`, formatPointCents(restored), userID).Scan(&balance).Error; err != nil {
			return false, err
		}
	}
	var refundLedgerID int64
	if err := tx.Raw(`INSERT INTO point_ledger(user_id,change_amount,balance_after,business_type,business_id,remark,idempotency_key)
		VALUES(?,?::numeric,?::numeric,?,?,?,?) RETURNING id`, userID, formatPointCents(restored), balance,
		businessType, businessID, remark, refundKey).Scan(&refundLedgerID).Error; err != nil {
		return false, err
	}
	for _, allocation := range restoredAllocations {
		if err := tx.Exec(`INSERT INTO point_ledger_allocations(ledger_id,grant_id,allocation_type,amount)
			VALUES(?,?,'refund',?::numeric)`, refundLedgerID, allocation.GrantID, allocation.Amount).Error; err != nil {
			return false, err
		}
	}
	return true, nil
}
