package payment

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"time"

	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"kra/internal/biz"
	"kra/pkg/database/pagination"
)

type paymentOrderPO struct {
	ID                     uint64 `gorm:"primaryKey"`
	CreatedAt              time.Time
	UpdatedAt              time.Time
	TradeNo                string  `gorm:"size:128;not null;uniqueIndex:idx_pay_orders_provider_trade"`
	Provider               string  `gorm:"size:64;not null;uniqueIndex:idx_pay_orders_provider_trade;uniqueIndex:idx_pay_orders_provider_provider_trade;index"`
	ProviderTradeNo        *string `gorm:"size:128;uniqueIndex:idx_pay_orders_provider_provider_trade"`
	QueryID                string  `gorm:"size:128"`
	BusinessType           string  `gorm:"size:64;not null;index:idx_pay_orders_business"`
	BusinessID             string  `gorm:"size:128;not null;index:idx_pay_orders_business"`
	Subject                string  `gorm:"size:256;not null"`
	PaymentMode            string  `gorm:"size:32;not null;default:external;index"`
	OriginalAmount         int64   `gorm:"not null;default:0"`
	Amount                 int64   `gorm:"not null"`
	PaidAmount             int64   `gorm:"not null;default:0"`
	PayerPaidAmount        int64   `gorm:"not null;default:0"`
	CashPaidAmount         int64   `gorm:"not null;default:0"`
	PointPaidAmount        int64   `gorm:"not null;default:0"`
	DiscountAmount         int64   `gorm:"not null;default:0"`
	ProviderDiscountAmount int64   `gorm:"not null;default:0"`
	MerchantDiscountAmount int64   `gorm:"not null;default:0"`
	SettlementAmount       int64   `gorm:"not null;default:0"`
	Currency               string  `gorm:"size:16;not null"`
	PayerCurrency          string  `gorm:"size:16"`
	AmountBreakdownKnown   bool    `gorm:"not null;default:false"`
	PaymentStatus          string  `gorm:"size:32;not null;index"`
	ProviderStatus         string  `gorm:"size:64"`
	FulfillmentStatus      string  `gorm:"size:32;not null;index"`
	RefundStatus           string  `gorm:"size:32;not null;index"`
	RefundedAmount         int64   `gorm:"not null;default:0"`
	RefundRequestedAmount  int64   `gorm:"not null;default:0"`
	RefundNo               string  `gorm:"size:128;index"`
	ConfirmationID         string  `gorm:"size:36;not null;uniqueIndex"`
	RequestFingerprint     string  `gorm:"size:64;not null;index"`
	CreatePayload          string  `gorm:"type:text"`
	Extra                  string  `gorm:"type:text"`
	LastEventID            string  `gorm:"size:128"`
	LastPayloadHash        string  `gorm:"size:64"`
	LastError              string  `gorm:"size:512"`
	FulfillmentToken       string  `gorm:"size:36"`
	RefundToken            string  `gorm:"size:36"`
	Version                uint64  `gorm:"not null;default:1"`
	PaidAt                 *time.Time
	FulfilledAt            *time.Time
	RefundedAt             *time.Time
	FulfillmentLeaseUntil  *time.Time
	RefundLeaseUntil       *time.Time
}

func (paymentOrderPO) TableName() string { return "pay_orders" }

type paymentOrderRepo struct{ data Provider }

func NewPaymentOrderRepo(data Provider) biz.PaymentOrderRepo {
	return &paymentOrderRepo{data: data}
}

func newPaymentOrderPO(order *biz.PaymentOrder) (*paymentOrderPO, error) {
	if order == nil {
		return nil, errors.New("支付订单为空")
	}
	extra := string(order.Extra)
	if extra == "" {
		extra = "{}"
	}
	createPayload := string(order.CreatePayload)
	return &paymentOrderPO{
		ID: order.ID, TradeNo: order.TradeNo, Provider: order.Provider,
		ProviderTradeNo: optionalString(order.ProviderTradeNo), QueryID: order.QueryID,
		BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject,
		PaymentMode: defaultString(order.PaymentMode, biz.PaymentModeExternal), OriginalAmount: order.OriginalAmount,
		Amount: order.Amount, PaidAmount: order.PaidAmount, PayerPaidAmount: order.PayerPaidAmount,
		CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount, DiscountAmount: order.DiscountAmount,
		ProviderDiscountAmount: order.ProviderDiscountAmount, MerchantDiscountAmount: order.MerchantDiscountAmount,
		SettlementAmount: order.SettlementAmount, Currency: order.Currency, PayerCurrency: order.PayerCurrency,
		AmountBreakdownKnown: order.AmountBreakdownKnown,
		PaymentStatus:        defaultString(order.PaymentStatus, biz.PaymentStatusInitialized),
		ProviderStatus:       order.ProviderStatus,
		FulfillmentStatus:    defaultString(order.FulfillmentStatus, biz.FulfillmentStatusPending),
		RefundStatus:         defaultString(order.RefundStatus, biz.RefundStatusNone),
		RefundedAmount:       order.RefundedAmount, RefundRequestedAmount: order.RefundRequestedAmount, RefundNo: order.RefundNo,
		ConfirmationID: order.ConfirmationID, RequestFingerprint: order.RequestFingerprint,
		CreatePayload: createPayload, Extra: extra, LastEventID: order.LastEventID,
		LastPayloadHash: order.LastPayloadHash, LastError: order.LastError,
		FulfillmentToken: order.FulfillmentToken, RefundToken: order.RefundToken, Version: defaultVersion(order.Version),
		PaidAt: order.PaidAt, FulfilledAt: order.FulfilledAt, RefundedAt: order.RefundedAt,
		FulfillmentLeaseUntil: order.FulfillmentLeaseUntil, RefundLeaseUntil: order.RefundLeaseUntil,
	}, nil
}

func toBizPaymentOrder(po *paymentOrderPO) *biz.PaymentOrder {
	if po == nil {
		return nil
	}
	return &biz.PaymentOrder{
		ID: po.ID, TradeNo: po.TradeNo, Provider: po.Provider,
		ProviderTradeNo: dereferenceString(po.ProviderTradeNo), QueryID: po.QueryID,
		BusinessType: po.BusinessType, BusinessID: po.BusinessID, Subject: po.Subject,
		PaymentMode: defaultString(po.PaymentMode, biz.PaymentModeExternal), OriginalAmount: po.OriginalAmount,
		Amount: po.Amount, PaidAmount: po.PaidAmount, PayerPaidAmount: po.PayerPaidAmount,
		CashPaidAmount: po.CashPaidAmount, PointPaidAmount: po.PointPaidAmount, DiscountAmount: po.DiscountAmount,
		ProviderDiscountAmount: po.ProviderDiscountAmount, MerchantDiscountAmount: po.MerchantDiscountAmount,
		SettlementAmount: po.SettlementAmount, Currency: po.Currency, PayerCurrency: po.PayerCurrency,
		AmountBreakdownKnown: po.AmountBreakdownKnown,
		PaymentStatus:        po.PaymentStatus, ProviderStatus: po.ProviderStatus,
		FulfillmentStatus: po.FulfillmentStatus, RefundStatus: po.RefundStatus,
		RefundedAmount: po.RefundedAmount, RefundRequestedAmount: po.RefundRequestedAmount, RefundNo: po.RefundNo,
		ConfirmationID: po.ConfirmationID, RequestFingerprint: po.RequestFingerprint,
		CreatePayload: json.RawMessage(po.CreatePayload), Extra: json.RawMessage(po.Extra),
		LastEventID: po.LastEventID, LastPayloadHash: po.LastPayloadHash, LastError: po.LastError,
		FulfillmentToken: po.FulfillmentToken, RefundToken: po.RefundToken, Version: po.Version, CreatedAt: po.CreatedAt,
		UpdatedAt: po.UpdatedAt, PaidAt: po.PaidAt, FulfilledAt: po.FulfilledAt,
		RefundedAt: po.RefundedAt, FulfillmentLeaseUntil: po.FulfillmentLeaseUntil, RefundLeaseUntil: po.RefundLeaseUntil,
	}
}

func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *biz.PaymentOrder) (*biz.PaymentOrder, bool, error) {
	po, err := newPaymentOrderPO(order)
	if err != nil {
		return nil, false, err
	}
	db := r.data.DB().WithContext(ctx)
	var current paymentOrderPO
	err = db.Where("provider = ? AND trade_no = ?", po.Provider, po.TradeNo).First(&current).Error
	if err == nil {
		return toBizPaymentOrder(&current), false, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, false, err
	}
	if err = db.Create(po).Error; err == nil {
		return toBizPaymentOrder(po), true, nil
	}
	if lookupErr := db.Where("provider = ? AND trade_no = ?", po.Provider, po.TradeNo).First(&current).Error; lookupErr == nil {
		return toBizPaymentOrder(&current), false, nil
	}
	return nil, false, err
}

func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*biz.PaymentOrder, error) {
	var po paymentOrderPO
	if err := r.data.DB().WithContext(ctx).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, biz.ErrPaymentOrderNotFound
		}
		return nil, err
	}
	return toBizPaymentOrder(&po), nil
}

func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize int, filter biz.PaymentOrderFilter) ([]*biz.PaymentOrder, int64, error) {
	db := r.data.DB().WithContext(ctx).Model(&paymentOrderPO{})
	if value := strings.TrimSpace(filter.Provider); value != "" {
		db = db.Where("provider = ?", value)
	}
	if value := strings.TrimSpace(filter.TradeNo); value != "" {
		db = db.Where("trade_no LIKE ?", "%"+value+"%")
	}
	if value := strings.TrimSpace(filter.BusinessType); value != "" {
		db = db.Where("business_type = ?", value)
	}
	if value := strings.TrimSpace(filter.BusinessID); value != "" {
		db = db.Where("business_id LIKE ?", "%"+value+"%")
	}
	if value := strings.TrimSpace(filter.PaymentStatus); value != "" {
		db = db.Where("payment_status = ?", value)
	}
	if value := strings.TrimSpace(filter.RefundStatus); value != "" {
		db = db.Where("refund_status = ?", value)
	}
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	var rows []paymentOrderPO
	if err := pagination.ApplyRequired(db.Order("id desc"), page, pageSize, 100).Find(&rows).Error; err != nil {
		return nil, 0, err
	}
	items := make([]*biz.PaymentOrder, 0, len(rows))
	for i := range rows {
		items = append(items, toBizPaymentOrder(&rows[i]))
	}
	return items, total, nil
}

func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *biz.PaymentProviderUpdate) (*biz.PaymentOrder, error) {
	return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
		if update == nil {
			return errors.New("支付下单结果为空")
		}
		if err := applyProviderIdentity(tx, po, update); err != nil {
			return err
		}
		if len(update.CreatePayload) > 0 {
			po.CreatePayload = string(update.CreatePayload)
		}
		po.ProviderStatus = trimTo(update.ProviderStatus, 64)
		po.LastEventID = trimTo(update.EventID, 128)
		po.LastPayloadHash = trimTo(update.PayloadHash, 64)
		status := normalizeOrderPaymentStatus(update.Status)
		if status == biz.PaymentStatusPaid {
			// Provider create responses are never sufficient proof of payment.
			status = biz.PaymentStatusPending
		}
		if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded && status != "" {
			po.PaymentStatus = status
		}
		po.Version++
		return tx.Save(po).Error
	})
}

func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tradeNo string, update *biz.PaymentProviderUpdate) (*biz.PaymentOrder, error) {
	return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
		if update == nil {
			return errors.New("支付查单结果为空")
		}
		if err := applyProviderIdentity(tx, po, update); err != nil {
			return err
		}
		if update.Amount > 0 && update.Amount != po.Amount {
			return biz.ErrPaymentOrderConflict
		}
		if update.PayerPaidAmount < 0 || update.CashPaidAmount < 0 || update.PointPaidAmount < 0 || update.DiscountAmount < 0 || update.ProviderDiscountAmount < 0 || update.MerchantDiscountAmount < 0 || update.SettlementAmount < 0 {
			return biz.ErrPaymentOrderConflict
		}
		if update.Currency != "" && !strings.EqualFold(update.Currency, po.Currency) {
			return biz.ErrPaymentOrderConflict
		}
		po.ProviderStatus = trimTo(update.ProviderStatus, 64)
		po.LastEventID = trimTo(update.EventID, 128)
		po.LastPayloadHash = trimTo(update.PayloadHash, 64)
		if update.AmountBreakdownKnown {
			po.PayerPaidAmount = update.PayerPaidAmount
			po.CashPaidAmount = update.CashPaidAmount
			po.PointPaidAmount = update.PointPaidAmount
			po.DiscountAmount = update.DiscountAmount
			po.ProviderDiscountAmount = update.ProviderDiscountAmount
			po.MerchantDiscountAmount = update.MerchantDiscountAmount
			po.SettlementAmount = update.SettlementAmount
			po.PayerCurrency = trimTo(update.PayerCurrency, 16)
			po.AmountBreakdownKnown = true
		}
		switch normalizeOrderPaymentStatus(update.Status) {
		case biz.PaymentStatusPaid:
			if po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
				po.PaymentStatus = biz.PaymentStatusPaid
			}
			po.PaidAmount = po.Amount
			if po.PaidAt == nil {
				now := time.Now().UTC()
				po.PaidAt = &now
			}
		case biz.PaymentStatusPending:
			if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
				po.PaymentStatus = biz.PaymentStatusPending
			}
		case biz.PaymentStatusFailed:
			if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
				po.PaymentStatus = biz.PaymentStatusFailed
			}
		}
		po.Version++
		return tx.Save(po).Error
	})
}

func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider, tradeNo string, lease time.Duration) (*biz.PaymentOrder, string, bool, error) {
	var token string
	var duplicate bool
	order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
		if po.FulfillmentStatus == biz.FulfillmentStatusSucceeded {
			duplicate = true
			return nil
		}
		if po.PaymentStatus != biz.PaymentStatusPaid {
			return biz.ErrPaymentOrderState
		}
		now := time.Now().UTC()
		if po.FulfillmentStatus == biz.FulfillmentStatusProcessing && po.FulfillmentLeaseUntil != nil && po.FulfillmentLeaseUntil.After(now) {
			return biz.ErrPaymentOrderBusy
		}
		if lease <= 0 {
			lease = 10 * time.Minute
		}
		token = uuid.NewString()
		until := now.Add(lease)
		po.FulfillmentStatus = biz.FulfillmentStatusProcessing
		po.FulfillmentToken = token
		po.FulfillmentLeaseUntil = &until
		po.LastError = ""
		po.Version++
		return tx.Save(po).Error
	})
	return order, token, duplicate, err
}

func (r *paymentOrderRepo) CompletePaymentFulfillment(ctx context.Context, provider, tradeNo, token string, success bool, message string) (*biz.PaymentOrder, error) {
	return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
		if po.FulfillmentStatus != biz.FulfillmentStatusProcessing || po.FulfillmentToken != token {
			return biz.ErrPaymentOrderBusy
		}
		po.FulfillmentToken = ""
		po.FulfillmentLeaseUntil = nil
		po.LastError = trimTo(message, 512)
		if success {
			po.FulfillmentStatus = biz.FulfillmentStatusSucceeded
			now := time.Now().UTC()
			po.FulfilledAt = &now
		} else {
			po.FulfillmentStatus = biz.FulfillmentStatusFailed
		}
		po.Version++
		return tx.Save(po).Error
	})
}

func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tradeNo string, amount int64, lease time.Duration) (*biz.PaymentOrder, string, error) {
	var token string
	order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
		if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded {
			return biz.ErrPaymentOrderState
		}
		if amount <= 0 {
			return biz.ErrPaymentOrderConflict
		}
		now := time.Now().UTC()
		if po.RefundStatus == biz.RefundStatusProcessing && po.RefundLeaseUntil != nil && po.RefundLeaseUntil.After(now) {
			return biz.ErrPaymentOrderBusy
		}
		if po.RefundStatus == biz.RefundStatusProcessing && po.RefundRequestedAmount != amount {
			return biz.ErrPaymentOrderConflict
		}
		if po.RefundStatus == biz.RefundStatusPending {
			return biz.ErrPaymentOrderBusy
		}
		// An expired processing lease means the provider outcome is unknown.
		// Retry the same refund amount with the same durable refund number. A
		// different amount must never reuse that operation identity.
		reserved := int64(0)
		if po.RefundStatus == biz.RefundStatusProcessing && po.RefundLeaseUntil != nil && !po.RefundLeaseUntil.After(now) {
			reserved = 0
		} else {
			reserved = po.RefundRequestedAmount
		}
		if amount > po.Amount-po.RefundedAmount-reserved {
			return biz.ErrPaymentOrderConflict
		}
		if lease <= 0 {
			lease = 10 * time.Minute
		}
		token = uuid.NewString()
		if po.RefundNo == "" {
			po.RefundNo = uuid.NewString()
		}
		until := now.Add(lease)
		po.RefundStatus = biz.RefundStatusProcessing
		po.RefundRequestedAmount = amount
		po.RefundToken = token
		po.RefundLeaseUntil = &until
		po.LastError = ""
		po.Version++
		return tx.Save(po).Error
	})
	return order, token, err
}

func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, provider, tradeNo, token string, accepted bool, message string) (*biz.PaymentOrder, error) {
	return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
		if po.RefundStatus != biz.RefundStatusProcessing || po.RefundToken != token {
			return biz.ErrPaymentOrderBusy
		}
		po.RefundToken = ""
		po.RefundLeaseUntil = nil
		po.LastError = trimTo(message, 512)
		if accepted {
			po.RefundStatus = biz.RefundStatusPending
		} else {
			po.RefundStatus = biz.RefundStatusFailed
			po.RefundRequestedAmount = 0
			po.RefundNo = ""
		}
		po.Version++
		return tx.Save(po).Error
	})
}

func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, tradeNo, refundNo string, amount int64, success bool, message string) (*biz.PaymentOrder, error) {
	return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
		if po.RefundStatus != biz.RefundStatusPending || po.RefundNo == "" || po.RefundNo != refundNo || po.RefundRequestedAmount != amount {
			return biz.ErrPaymentOrderState
		}
		po.LastError = trimTo(message, 512)
		po.RefundRequestedAmount = 0
		if !success {
			po.RefundStatus = biz.RefundStatusFailed
			po.RefundNo = ""
			po.Version++
			return tx.Save(po).Error
		}
		po.RefundedAmount += amount
		if po.RefundedAmount >= po.Amount {
			po.PaymentStatus = biz.PaymentStatusRefunded
			po.RefundStatus = biz.RefundStatusSucceeded
		} else {
			po.PaymentStatus = biz.PaymentStatusPartiallyRefunded
			po.RefundStatus = biz.RefundStatusPartial
		}
		po.RefundNo = ""
		now := time.Now().UTC()
		po.RefundedAt = &now
		po.Version++
		return tx.Save(po).Error
	})
}

func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*biz.PaymentOrder, error) {
	var result *biz.PaymentOrder
	err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var po paymentOrderPO
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return biz.ErrPaymentOrderNotFound
			}
			return err
		}
		if err := fn(tx, &po); err != nil {
			return err
		}
		result = toBizPaymentOrder(&po)
		return nil
	})
	return result, err
}

func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *biz.PaymentProviderUpdate) error {
	if update.ProviderTradeNo != "" {
		if po.ProviderTradeNo != nil && *po.ProviderTradeNo != update.ProviderTradeNo {
			return biz.ErrPaymentProviderConflict
		}
		var other paymentOrderPO
		// This check is repeated under the order transaction so a platform
		// transaction cannot be attached to a different merchant order.
		if err := tx.Where("provider = ? AND provider_trade_no = ? AND id <> ?", po.Provider, update.ProviderTradeNo, po.ID).First(&other).Error; err == nil {
			return biz.ErrPaymentProviderConflict
		} else if !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		value := update.ProviderTradeNo
		po.ProviderTradeNo = &value
	}
	if update.QueryID != "" {
		if po.QueryID != "" && po.QueryID != update.QueryID {
			return biz.ErrPaymentProviderConflict
		}
		po.QueryID = update.QueryID
	}
	return nil
}

func normalizeOrderPaymentStatus(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "success", "paid", "fulfilled":
		return biz.PaymentStatusPaid
	case "pending", "created", "client_pending", "processing":
		return biz.PaymentStatusPending
	case "failed", "closed", "cancelled", "canceled":
		return biz.PaymentStatusFailed
	default:
		return ""
	}
}

func optionalString(value string) *string {
	if value == "" {
		return nil
	}
	return &value
}

func dereferenceString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

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

func defaultVersion(value uint64) uint64 {
	if value == 0 {
		return 1
	}
	return value
}

func trimTo(value string, max int) string {
	value = strings.TrimSpace(value)
	if len(value) > max {
		return value[:max]
	}
	return value
}
