package payment

import (
	"context"
	"encoding/json"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"hfb_sys/backend/internal/model"
	"hfb_sys/backend/internal/timeutil"
	"log"
	"time"
)

// Start 发起订单支付，并按请求支付方式选择对应默认渠道配置。
func (r *Repository) Start(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) {
	req.PayWay = normalizePayWay(req.PayWay)
	defaultConfig, err := r.defaultRuntimeConfig(ctx, req.PayWay)
	if err != nil {
		return nil, ErrPaymentUnavailable
	}
	payment, orderRow, err := r.preparePayment(ctx, userID, orderID, req, *defaultConfig)
	if err != nil {
		return nil, err
	}
	runtimeConfig, err := r.runtimeConfigForPayment(ctx, payment)
	if err != nil {
		return nil, ErrPaymentUnavailable
	}
	if payment.Status == "paid" {
		r.recordConfigUsage(ctx, runtimeConfig, payment)
		dto := toDTO(*payment)
		return &dto, nil
	}
	if runtimeConfig.isMockMode() {
		if err := r.confirmPaid(ctx, payment, "2", time.Now(), map[string]string{
			"mock":            "true",
			"third_order_id":  payment.ThirdOrderID,
			"leshua_order_id": payment.ProviderOrderID,
			"status":          "2",
		}, channelSourceMock); err != nil {
			return nil, err
		}
		latest, err := r.findPaymentByID(ctx, payment.ID)
		if err != nil {
			return nil, err
		}
		r.recordConfigUsage(ctx, runtimeConfig, latest)
		dto := toDTO(*latest)
		return &dto, nil
	}
	if payment.Status == "paying" && (payment.TDCode != "" || payment.JSPayURL != "" || payment.JSPayInfo != "") {
		r.recordConfigUsage(ctx, runtimeConfig, payment)
		dto := toDTO(*payment)
		return &dto, nil
	}
	if runtimeConfig.Channel == nil {
		_ = r.markPaymentFailed(ctx, payment.ID, nil, "payment channel unavailable")
		return nil, ErrPaymentUnavailable
	}

	log.Printf("[payment] payment start order_id=%d order_no=%s payment_id=%d provider=%s amount_cent=%d third_order_id=%s",
		orderID, orderRow.OrderNo, payment.ID, runtimeConfig.Provider, payment.AmountCent, payment.ThirdOrderID)
	resp, err := runtimeConfig.Channel.CreatePayment(ctx, channelCreatePaymentRequest{
		ThirdOrderID: payment.ThirdOrderID,
		AmountCent:   payment.AmountCent,
		PayWay:       payment.PayWay,
		JSPayFlag:    payment.JSPayFlag,
		NotifyURL:    runtimeConfig.NotifyURL,
		JumpURL:      runtimeConfig.JumpURL,
		ClientIP:     clientIP,
		Body:         "租号订单 " + orderRow.OrderNo,
		Attach:       orderRow.OrderNo,
	})
	if err != nil {
		_ = r.markPaymentFailed(ctx, payment.ID, nil, err.Error())
		log.Printf("[payment] payment request failed order_id=%d payment_id=%d provider=%s amount_cent=%d err=%v",
			orderID, payment.ID, runtimeConfig.Provider, payment.AmountCent, err)
		return nil, err
	}
	if !resp.OK {
		_ = r.markPaymentFailed(ctx, payment.ID, resp.Raw, resp.ErrorMessage)
		log.Printf("[payment] payment rejected order_id=%d payment_id=%d provider=%s amount_cent=%d code=%s message=%s",
			orderID, payment.ID, runtimeConfig.Provider, payment.AmountCent, firstNonEmpty(resp.Raw["code"], resp.Raw["resp_code"], resp.Raw["result_code"]), resp.ErrorMessage)
		return nil, ErrPaymentUnavailable
	}
	if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{
		"status":            "paying",
		"provider_order_id": resp.ProviderOrderID,
		"pay_way":           firstNonEmpty(resp.PayWay, payment.PayWay),
		"td_code":           resp.TDCode,
		"jspay_url":         resp.JSPayURL,
		"jspay_info":        resp.JSPayInfo,
		"raw_request":       jsonMap(resp.RawRequest),
		"raw_response":      jsonMap(withRawSource(resp.Raw, channelSourceCreate)),
	}).Error; err != nil {
		return nil, err
	}
	latest, err := r.findPaymentByID(ctx, payment.ID)
	if err != nil {
		return nil, err
	}
	r.recordConfigUsage(ctx, runtimeConfig, latest)
	log.Printf("[payment] payment result order_id=%d order_no=%s payment_id=%d provider=%s amount_cent=%d status=%s provider_order_id=%s",
		orderID, orderRow.OrderNo, latest.ID, runtimeConfig.Provider, latest.AmountCent, latest.Status, latest.ProviderOrderID)
	dto := toDTO(*latest)
	return &dto, nil
}

// preparePayment 创建或复用订单支付单，并写入本次命中的支付配置 ID。
func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (*model.PaymentOrder, *model.RentalOrder, error) {
	var paymentID uint64
	var orderRow model.RentalOrder
	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var row model.RentalOrder
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("id = ? AND renter_id = ?", orderID, userID).
			First(&row).Error; err != nil {
			return err
		}
		if row.Status != "pending_payment" {
			return ErrPaymentCannotStart
		}
		amountCent := row.RentAmountCent + row.DepositAmountCent
		if amountCent <= 0 {
			return ErrPaymentCannotStart
		}
		var existing model.PaymentOrder
		err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("order_id = ? AND biz_type = ?", row.ID, "order_pay").
			Order("id DESC").
			First(&existing).Error
		if err == nil {
			if canReuseOrderPayment(existing, runtimeConfig) {
				existing.PayWay = firstNonEmpty(req.PayWay, existing.PayWay, runtimeConfig.PayWay, "ZFBZF")
				existing.JSPayFlag = firstNonEmpty(req.JSPayFlag, existing.JSPayFlag, runtimeConfig.JSPayFlag, "2")
				existing.AmountCent = amountCent
				existing.PaymentConfigID = firstNonZero(existing.PaymentConfigID, runtimeConfig.ID)
				existing.Provider = firstNonEmpty(existing.Provider, runtimeConfig.Provider)
				existing.MerchantID = firstNonEmpty(existing.MerchantID, runtimeConfig.MerchantID)
				if existing.Provider == "mock" && existing.ProviderOrderID == "" {
					existing.ProviderOrderID = "MOCK" + existing.ThirdOrderID
				}
				if err := tx.Save(&existing).Error; err != nil {
					return err
				}
				paymentID = existing.ID
				orderRow = row
				return nil
			}
		} else if err != gorm.ErrRecordNotFound {
			return err
		}
		payment, err := newOrderPayment(row, amountCent, req, runtimeConfig)
		if err != nil {
			return err
		}
		if err := tx.Create(&payment).Error; err != nil {
			return err
		}
		paymentID = payment.ID
		orderRow = row
		return nil
	})
	if err != nil {
		return nil, nil, err
	}
	payment, err := r.findPaymentByID(ctx, paymentID)
	if err != nil {
		return nil, nil, err
	}
	return payment, &orderRow, nil
}

// canReuseOrderPayment 判断旧支付单是否还能复用，支付方式或配置不同则必须重新下单。
func canReuseOrderPayment(payment model.PaymentOrder, runtimeConfig runtimePaymentConfig) bool {
	if payment.Status == "paid" {
		return true
	}
	if payment.Status != "created" && payment.Status != "paying" {
		return false
	}
	if payment.Provider != "" && runtimeConfig.Provider != "" && payment.Provider != runtimeConfig.Provider {
		return false
	}
	if payment.MerchantID != "" && runtimeConfig.MerchantID != "" && payment.MerchantID != runtimeConfig.MerchantID {
		return false
	}
	if payment.PaymentConfigID != 0 && runtimeConfig.ID != 0 && payment.PaymentConfigID != runtimeConfig.ID {
		return false
	}
	if payment.PayWay != "" && runtimeConfig.PayWay != "" && normalizePayWay(payment.PayWay) != normalizePayWay(runtimeConfig.PayWay) {
		return false
	}
	if payment.Status == "paying" && paymentCashierExpired(payment) {
		return false
	}
	return true
}

// paymentCashierExpired 判断渠道收银台是否已过期，过期后不再复用支付单。
func paymentCashierExpired(payment model.PaymentOrder) bool {
	if len(payment.RawRequest) == 0 {
		return false
	}
	var raw map[string]string
	if err := json.Unmarshal(payment.RawRequest, &raw); err != nil {
		return false
	}
	deadline := parseChannelTime(raw["order_efficient_time"])
	if deadline == nil {
		return false
	}
	return !timeutil.ShanghaiNow().Before(*deadline)
}

// newOrderPayment 创建新的订单支付单，并固化支付方式和支付配置 ID。
func newOrderPayment(row model.RentalOrder, amountCent int64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (model.PaymentOrder, error) {
	paymentNo, err := newPaymentNo()
	if err != nil {
		return model.PaymentOrder{}, err
	}
	payment := model.PaymentOrder{
		PaymentNo:       paymentNo,
		OrderID:         row.ID,
		OrderNo:         row.OrderNo,
		UserID:          row.RenterID,
		PaymentConfigID: runtimeConfig.ID,
		Provider:        runtimeConfig.Provider,
		MerchantID:      runtimeConfig.MerchantID,
		ThirdOrderID:    paymentNo,
		ProviderOrderID: "",
		PayWay:          firstNonEmpty(req.PayWay, runtimeConfig.PayWay, "ZFBZF"),
		JSPayFlag:       firstNonEmpty(req.JSPayFlag, runtimeConfig.JSPayFlag, "2"),
		AmountCent:      amountCent,
		BizType:         "order_pay",
		Status:          "created",
	}
	if runtimeConfig.isMockMode() {
		payment.ProviderOrderID = "MOCK" + paymentNo
		payment.TDCode = "mock://payment/pay/" + paymentNo
	}
	return payment, nil
}
