package payment

import (
	"context"
	"encoding/json"
	"log"
	"time"

	"gorm.io/datatypes"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"hfb_sys/backend/internal/model"
)

func (r *Repository) StartRefund(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (*RefundDTO, error) {
	originalPayment, err := r.findOriginalPayment(ctx, orderID)
	if err != nil {
		return nil, err
	}
	runtimeConfig, err := r.runtimeConfigForPayment(ctx, &originalPayment)
	if err != nil {
		return nil, ErrPaymentUnavailable
	}

	refundOrder, existing, err := r.prepareRefundOrder(ctx, originalPayment, *runtimeConfig, refundAmountCent, bizType)
	if err != nil {
		return nil, err
	}
	if existing {
		latest, syncErr := r.syncRefundPayment(ctx, refundOrder, refundOrder.Status != "refunded")
		if syncErr != nil {
			log.Printf("[payment] sync existing refund failed order_id=%d payment_id=%d biz_type=%s err=%v", orderID, refundOrder.ID, bizType, syncErr)
			dto := toRefundDTO(*refundOrder)
			return &dto, nil
		}
		dto := toRefundDTO(*latest)
		return &dto, nil
	}

	if runtimeConfig.isMockMode() {
		raw := map[string]string{"mock": "true"}
		if remark != "" {
			raw["remark"] = remark
		}
		if err := r.applyRefundChannelStatus(ctx, refundOrder, refundChannelStatusUpdate{
			Status:           "refunded",
			ProviderRefundID: "MOCKREF" + refundOrder.ThirdOrderID,
			Raw:              raw,
			Source:           channelSourceMock,
		}); err != nil {
			return nil, err
		}
		latest, err := r.findPaymentByID(ctx, refundOrder.ID)
		if err != nil {
			return nil, err
		}
		r.recordConfigUsage(ctx, runtimeConfig, latest)
		dto := toRefundDTO(*latest)
		return &dto, nil
	}

	log.Printf("[payment] refund start order_id=%d order_no=%s payment_id=%d biz_type=%s provider=%s amount_cent=%d merchant_refund_id=%s origin_third_order_id=%s origin_provider_order_id=%s",
		orderID, originalPayment.OrderNo, refundOrder.ID, bizType, runtimeConfig.Provider, refundAmountCent, refundOrder.ThirdOrderID, originalPayment.ThirdOrderID, refundOriginProviderOrderID(originalPayment))
	r.recordConfigUsage(ctx, runtimeConfig, refundOrder)
	if err := r.markOrderRefunding(ctx, orderID, refundAmountCent); err != nil {
		log.Printf("[payment] mark order refunding failed order_id=%d err=%v", orderID, err)
	}

	if runtimeConfig.Channel == nil {
		if err := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, map[string]string{"error": "payment channel unavailable"}); err != nil {
			log.Printf("[payment] mark refund failed status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, err)
		}
		return nil, ErrPaymentUnavailable
	}
	resp, err := runtimeConfig.Channel.CreateRefund(ctx, channelCreateRefundRequest{
		ThirdOrderID:     originalPayment.ThirdOrderID,
		ProviderOrderID:  refundOriginProviderOrderID(originalPayment),
		MerchantRefundID: refundOrder.ThirdOrderID,
		RefundAmountCent: refundAmountCent,
		NotifyURL:        runtimeConfig.NotifyURL,
		Attach:           originalPayment.OrderNo,
		Remark:           remark,
	})
	if err != nil {
		if markErr := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, map[string]string{"error": err.Error()}); markErr != nil {
			log.Printf("[payment] mark refund failed status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, markErr)
		}
		log.Printf("[payment] refund request failed order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d err=%v",
			orderID, refundOrder.ID, bizType, runtimeConfig.Provider, refundAmountCent, err)
		return nil, err
	}
	if !resp.OK {
		if markErr := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, resp.Raw); markErr != nil {
			log.Printf("[payment] mark refund rejected status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, markErr)
		}
		log.Printf("[payment] refund rejected order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d code=%s message=%s",
			orderID, refundOrder.ID, bizType, runtimeConfig.Provider, refundAmountCent, firstNonEmpty(resp.Raw["code"], resp.Raw["resp_code"], resp.Raw["result_code"]), resp.ErrorMessage)
		return nil, ErrPaymentUnavailable
	}

	if err := r.applyRefundChannelStatus(ctx, refundOrder, refundChannelStatusUpdate{
		Status:           resp.Status,
		ProviderRefundID: resp.ProviderRefundID,
		RawRequest:       resp.RawRequest,
		Raw:              resp.Raw,
		Source:           channelSourceCreate,
	}); err != nil {
		return nil, err
	}
	log.Printf("[payment] refund result order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d status=%s provider_refund_id=%s",
		orderID, refundOrder.ID, bizType, runtimeConfig.Provider, refundAmountCent, resp.Status, resp.ProviderRefundID)

	latest, err := r.findPaymentByID(ctx, refundOrder.ID)
	if err != nil {
		return nil, err
	}
	dto := toRefundDTO(*latest)
	return &dto, nil
}
func (r *Repository) QueryRefundStatus(ctx context.Context, orderID uint64) (*RefundDTO, error) {
	var payment model.PaymentOrder
	if err := r.db.WithContext(ctx).Where("order_id = ? AND biz_type IN ?", orderID, refundBizTypes).Order("id DESC").First(&payment).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, ErrPaymentNotFound
		}
		return nil, err
	}
	latest, err := r.syncRefundPayment(ctx, &payment, payment.Status == "failed")
	if err != nil {
		return nil, err
	}
	dto := toRefundDTO(*latest)
	return &dto, nil
}
func (r *Repository) SyncRefundStatusByPaymentID(ctx context.Context, paymentID uint64) (*RefundDTO, error) {
	payment, err := r.findPaymentByID(ctx, paymentID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, ErrPaymentNotFound
		}
		return nil, err
	}
	if !isRefundBizType(payment.BizType) {
		return nil, ErrPaymentNotFound
	}
	latest, err := r.syncRefundPayment(ctx, payment, true)
	if err != nil {
		return nil, err
	}
	dto := toRefundDTO(*latest)
	return &dto, nil
}
func (r *Repository) findOriginalPayment(ctx context.Context, orderID uint64) (model.PaymentOrder, error) {
	var originalPayment model.PaymentOrder
	if err := r.db.WithContext(ctx).Where("order_id = ? AND status = 'paid' AND biz_type = 'order_pay'", orderID).Order("id DESC").First(&originalPayment).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return originalPayment, ErrPaymentNotFound
		}
		return originalPayment, err
	}
	return originalPayment, nil
}

// prepareRefundOrder 创建退款支付单，并继承原支付单回溯出的支付配置。
func (r *Repository) prepareRefundOrder(ctx context.Context, originalPayment model.PaymentOrder, runtimeConfig runtimePaymentConfig, refundAmountCent int64, bizType string) (*model.PaymentOrder, bool, error) {
	var paymentID uint64
	existing := false
	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var order model.RentalOrder
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, originalPayment.OrderID).Error; err != nil {
			return err
		}
		var existingRefund model.PaymentOrder
		err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("order_id = ? AND biz_type = ?", originalPayment.OrderID, bizType).
			Order("id DESC").
			First(&existingRefund).Error
		if err == nil {
			paymentID = existingRefund.ID
			existing = true
			return nil
		}
		if err != gorm.ErrRecordNotFound {
			return err
		}
		paymentNo, err := newPaymentNo()
		if err != nil {
			return err
		}
		merchantRefundID := "REF" + paymentNo[3:]
		refundOrder := model.PaymentOrder{
			PaymentNo:       paymentNo,
			OrderID:         originalPayment.OrderID,
			OrderNo:         originalPayment.OrderNo,
			UserID:          originalPayment.UserID,
			PaymentConfigID: runtimeConfig.ID,
			Provider:        runtimeConfig.Provider,
			MerchantID:      runtimeConfig.MerchantID,
			ThirdOrderID:    merchantRefundID,
			ProviderOrderID: "",
			PayWay:          originalPayment.PayWay,
			JSPayFlag:       originalPayment.JSPayFlag,
			AmountCent:      refundAmountCent,
			BizType:         bizType,
			Status:          "refunding",
		}
		if err := tx.Create(&refundOrder).Error; err != nil {
			return err
		}
		paymentID = refundOrder.ID
		return nil
	})
	if err != nil {
		return nil, false, err
	}
	payment, err := r.findPaymentByID(ctx, paymentID)
	if err != nil {
		return nil, false, err
	}
	return payment, existing, nil
}
func (r *Repository) syncRefundPayment(ctx context.Context, payment *model.PaymentOrder, queryTerminal bool) (*model.PaymentOrder, error) {
	if payment.Status == "refunded" && !queryTerminal {
		if err := r.updateOrderRefundStatus(ctx, payment.OrderID, payment.AmountCent); err != nil {
			return nil, err
		}
		return payment, nil
	}
	if payment.Status == "failed" && !queryTerminal {
		if err := r.markOrderRefundFailed(ctx, payment.OrderID, payment.AmountCent); err != nil {
			return nil, err
		}
		return payment, nil
	}
	runtimeConfig, err := r.runtimeConfigForPayment(ctx, payment)
	if err != nil {
		return nil, ErrPaymentUnavailable
	}
	if runtimeConfig.isMockMode() {
		if payment.Status == "refunded" {
			if err := r.updateOrderRefundStatus(ctx, payment.OrderID, payment.AmountCent); err != nil {
				return nil, err
			}
		}
		return payment, nil
	}
	if runtimeConfig.Channel == nil {
		return nil, ErrPaymentUnavailable
	}
	originalPayment, err := r.findOriginalPayment(ctx, payment.OrderID)
	if err != nil {
		return nil, err
	}
	resp, err := runtimeConfig.Channel.QueryRefund(ctx, refundQueryRequest(*payment, originalPayment))
	if err != nil {
		return nil, err
	}
	if !resp.OK {
		return nil, ErrPaymentUnavailable
	}
	if err := r.applyRefundChannelStatus(ctx, payment, refundChannelStatusUpdate{
		Status:           resp.Status,
		ProviderRefundID: resp.ProviderRefundID,
		RefundTime:       resp.RefundTime,
		Raw:              resp.Raw,
		Source:           channelSourceQuery,
	}); err != nil {
		return nil, err
	}
	return r.findPaymentByID(ctx, payment.ID)
}

type refundChannelStatusUpdate struct {
	Status           string
	ProviderRefundID string
	RefundTime       string
	RawRequest       map[string]string
	Raw              map[string]string
	Source           string
}

func (r *Repository) applyRefundChannelStatus(ctx context.Context, payment *model.PaymentOrder, update refundChannelStatusUpdate) error {
	status := update.Status
	if status != "refunded" && status != "failed" {
		status = "refunding"
	}
	updates := map[string]any{
		"status":       status,
		"raw_response": jsonMap(withRawSource(update.Raw, update.Source)),
	}
	if update.ProviderRefundID != "" {
		updates["provider_order_id"] = update.ProviderRefundID
	}
	if update.RawRequest != nil {
		updates["raw_request"] = jsonMap(update.RawRequest)
	}
	if status == "refunded" {
		paidAt := parseChannelTime(update.RefundTime)
		if paidAt == nil {
			now := time.Now()
			paidAt = &now
		}
		updates["paid_at"] = paidAt
	}
	if update.Source == channelSourceNotify {
		updates["notified_at"] = time.Now()
	}
	if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(updates).Error; err != nil {
		return err
	}
	switch status {
	case "refunded":
		return r.updateOrderRefundStatus(ctx, payment.OrderID, payment.AmountCent)
	case "failed":
		return r.markOrderRefundFailed(ctx, payment.OrderID, payment.AmountCent)
	default:
		return r.markOrderRefunding(ctx, payment.OrderID, payment.AmountCent)
	}
}
func isRefundBizType(bizType string) bool {
	for _, item := range refundBizTypes {
		if item == bizType {
			return true
		}
	}
	return false
}
func refundQueryRequest(payment model.PaymentOrder, originalPayment model.PaymentOrder) channelQueryRefundRequest {
	return channelQueryRefundRequest{
		ThirdOrderID:     originalPayment.ThirdOrderID,
		ProviderOrderID:  refundOriginProviderOrderID(originalPayment),
		MerchantRefundID: payment.ThirdOrderID,
		ProviderRefundID: payment.ProviderOrderID,
	}
}
func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64, refundAmountCent int64) error {
	now := time.Now()
	return r.db.WithContext(ctx).Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
		"refund_status":      "refunded",
		"refund_amount_cent": refundAmountCent,
		"refunded_at":        now,
	}).Error
}
func (r *Repository) markOrderRefunding(ctx context.Context, orderID uint64, refundAmountCent int64) error {
	return r.db.WithContext(ctx).Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
		"refund_status":      "refunding",
		"refund_amount_cent": refundAmountCent,
		"refunded_at":        nil,
	}).Error
}
func (r *Repository) markOrderRefundFailed(ctx context.Context, orderID uint64, refundAmountCent int64) error {
	return r.db.WithContext(ctx).Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
		"refund_status":      "failed",
		"refund_amount_cent": refundAmountCent,
		"refunded_at":        nil,
	}).Error
}
func (r *Repository) markRefundFailed(ctx context.Context, paymentID uint64, orderID uint64, refundAmountCent int64, raw map[string]string) error {
	if raw == nil {
		raw = map[string]string{"error": "refund failed"}
	}
	if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", paymentID).Updates(map[string]any{
		"status":       "failed",
		"raw_response": jsonMap(raw),
	}).Error; err != nil {
		return err
	}
	return r.markOrderRefundFailed(ctx, orderID, refundAmountCent)
}
func toRefundDTO(payment model.PaymentOrder) RefundDTO {
	return RefundDTO{
		ID:              payment.ID,
		PaymentNo:       payment.PaymentNo,
		OrderID:         payment.OrderID,
		OrderNo:         payment.OrderNo,
		BizType:         payment.BizType,
		AmountCent:      payment.AmountCent,
		Status:          payment.Status,
		ProviderOrderID: payment.ProviderOrderID,
		PaidAt:          payment.PaidAt,
		CreatedAt:       payment.CreatedAt,
		UpdatedAt:       payment.UpdatedAt,
	}
}
func refundOriginProviderOrderID(payment model.PaymentOrder) string {
	if payment.Provider != "lakala" {
		return payment.ProviderOrderID
	}
	if tradeID := lakalaOriginTradeID(payment.RawResponse); tradeID != "" {
		return tradeID
	}
	return payment.ProviderOrderID
}
func lakalaOriginTradeID(raw datatypes.JSON) string {
	if len(raw) == 0 {
		return ""
	}
	var payload map[string]any
	if err := json.Unmarshal(raw, &payload); err != nil {
		return ""
	}
	if tradeID := firstStringValue(payload, "trade_no", "origin_trade_no"); tradeID != "" {
		return tradeID
	}
	value, ok := payload["order_trade_info_list"]
	if !ok {
		return ""
	}
	switch typed := value.(type) {
	case string:
		var items []map[string]any
		if err := json.Unmarshal([]byte(typed), &items); err != nil {
			return ""
		}
		for _, item := range items {
			if tradeID := firstStringValue(item, "trade_no", "origin_trade_no"); tradeID != "" {
				return tradeID
			}
		}
	case []any:
		for _, item := range typed {
			itemMap, ok := item.(map[string]any)
			if !ok {
				continue
			}
			if tradeID := firstStringValue(itemMap, "trade_no", "origin_trade_no"); tradeID != "" {
				return tradeID
			}
		}
	}
	return ""
}
func firstStringValue(values map[string]any, keys ...string) string {
	for _, key := range keys {
		value, ok := values[key]
		if !ok {
			continue
		}
		if text, ok := value.(string); ok && text != "" {
			return text
		}
	}
	return ""
}
