package order

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

	"github.com/hibiken/asynq"
	"github.com/perfect-panel/server/internal/model/log"
	"github.com/perfect-panel/server/internal/model/user"
	"github.com/perfect-panel/server/pkg/payment/epay"
	"github.com/perfect-panel/server/pkg/payment/stripe"
	queueTypes "github.com/perfect-panel/server/queue/types"
	"gorm.io/gorm"

	"github.com/perfect-panel/server/internal/model/order"
	"github.com/perfect-panel/server/internal/model/payment"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/internal/types"
	"github.com/perfect-panel/server/pkg/logger"
	"github.com/perfect-panel/server/pkg/payment/alipay"
)

// GatewayPaymentStatus is the tri-state result of an external payment-gateway
// query. The third state (Unknown) is the whole point of HIF-137: when the
// gateway is unreachable or returns an inconclusive answer we must NOT collapse
// it to "unpaid" — that's exactly the bug that closes already-paid orders.
type GatewayPaymentStatus int

const (
	// GatewayStatusUnknown — the gateway query itself failed (timeout, 5xx,
	// network error, decode error) or the payment method has no gateway we can
	// query. The caller must treat this as "do not close, retry later".
	GatewayStatusUnknown GatewayPaymentStatus = iota
	// GatewayStatusPaid — the gateway confirms the user has paid.
	GatewayStatusPaid
	// GatewayStatusUnpaid — the gateway confirms the order is unpaid /
	// cancelled / closed on its side. Safe to close locally.
	GatewayStatusUnpaid
)

type CloseOrderLogic struct {
	logger.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext

	// Test seams (injected via overrides on the returned struct). Production
	// code never touches these — NewCloseOrderLogic wires the real impls.
	gatewayQuery    func(*order.Order) GatewayPaymentStatus
	enqueueActivate func(context.Context, []byte) (string, error)
}

// NewCloseOrderLogic Close order
func NewCloseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CloseOrderLogic {
	l := &CloseOrderLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
	l.gatewayQuery = l.queryGatewayPaymentStatus
	l.enqueueActivate = func(c context.Context, payload []byte) (string, error) {
		task := asynq.NewTask(queueTypes.ForthwithActivateOrder, payload, asynq.MaxRetry(5))
		info, err := svcCtx.Queue.EnqueueContext(c, task)
		if err != nil {
			return "", err
		}
		return info.ID, nil
	}
	return l
}

func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error {
	// Find order information by order number
	orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
	if err != nil {
		l.Errorw("[CloseOrder] Find order info failed",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", req.OrderNo),
		)
		return nil
	}
	// If the order status is not 1, it means that the order has been closed or paid
	if orderInfo.Status != 1 {
		l.Infow("[CloseOrder] Order status is not 1",
			logger.Field("orderNo", req.OrderNo),
			logger.Field("status", orderInfo.Status),
		)
		return nil
	}

	// HIF-137: before closing a pending order, ask the gateway whether it was
	// actually paid. Silent callback failures must not cause us to close a
	// paid order. Three branches:
	//   - Paid    → recover to status=2 + enqueue activation (do NOT close)
	//   - Unpaid  → fall through to the normal close flow
	//   - Unknown → keep status=1 and let DeferCloseOrder retry on the next tick
	switch l.gatewayQuery(orderInfo) {
	case GatewayStatusPaid:
		return l.recoverPaidOrder(orderInfo)
	case GatewayStatusUnknown:
		l.Errorw("[CloseOrder] gateway query inconclusive — keeping order open (metric=close_gateway_error_kept_open)",
			logger.Field("metric", "close_gateway_error_kept_open"),
			logger.Field("orderNo", orderInfo.OrderNo),
			logger.Field("method", orderInfo.Method),
			logger.Field("trade_no", orderInfo.TradeNo),
		)
		return nil
	}

	l.Infow("[CloseOrder] gateway confirmed unpaid — proceeding with normal close (metric=close_normal)",
		logger.Field("metric", "close_normal"),
		logger.Field("orderNo", orderInfo.OrderNo),
		logger.Field("method", orderInfo.Method),
	)

	sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, orderInfo.SubscribeId)
	if err != nil {
		l.Errorw("[CloseOrder] Find subscribe info failed",
			logger.Field("error", err.Error()),
			logger.Field("subscribeId", orderInfo.SubscribeId),
		)
		return nil
	}

	err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
		// update order status
		err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error
		if err != nil {
			l.Errorw("[CloseOrder] Update order status failed",
				logger.Field("error", err.Error()),
				logger.Field("orderNo", req.OrderNo),
			)
			return err
		}
		// If User ID is 0, it means that the order is a guest order and does not need to be refunded, the order can be deleted directly
		if orderInfo.UserId == 0 {
			err = tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Delete(&order.Order{}).Error
			if err != nil {
				l.Errorw("[CloseOrder] Delete order failed",
					logger.Field("error", err.Error()),
					logger.Field("orderNo", req.OrderNo),
				)
				return err
			}
			return nil
		}
		// refund deduction amount to user deduction balance
		if orderInfo.GiftAmount > 0 {
			userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId)
			if err != nil {
				l.Errorw("[CloseOrder] Find user info failed",
					logger.Field("error", err.Error()),
					logger.Field("user_id", orderInfo.UserId),
				)
				return err
			}
			deduction := userInfo.GiftAmount + orderInfo.GiftAmount
			err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("gift_amount", deduction).Error
			if err != nil {
				l.Errorw("[CloseOrder] Refund deduction amount failed",
					logger.Field("error", err.Error()),
					logger.Field("uid", orderInfo.UserId),
					logger.Field("deduction", orderInfo.GiftAmount),
				)
				return err
			}
			// Record the deduction refund log

			giftLog := log.Gift{
				Type:        log.GiftTypeIncrease,
				OrderNo:     orderInfo.OrderNo,
				SubscribeId: 0,
				Amount:      orderInfo.GiftAmount,
				Balance:     deduction,
				Remark:      "Order cancellation refund",
				Timestamp:   time.Now().UnixMilli(),
			}
			content, _ := giftLog.Marshal()

			err = tx.Model(&log.SystemLog{}).Create(&log.SystemLog{
				Id:       0,
				Type:     log.TypeGift.Uint8(),
				Date:     time.Now().Format(time.DateOnly),
				ObjectID: userInfo.Id,
				Content:  string(content),
			}).Error
			if err != nil {
				l.Errorw("[CloseOrder] Record cancellation refund log failed",
					logger.Field("error", err.Error()),
					logger.Field("uid", orderInfo.UserId),
					logger.Field("deduction", orderInfo.GiftAmount),
				)
				return err
			}
		}
		// Note: user cache will be updated after transaction commits
		if sub.Inventory != -1 {
			sub.Inventory++
			if e := l.svcCtx.SubscribeModel.Update(l.ctx, sub, tx); e != nil {
				l.Errorw("[CloseOrder] Restore subscribe inventory failed",
					logger.Field("error", e.Error()),
					logger.Field("subscribeId", sub.Id),
				)
				return e
			}
		}

		return nil
	})
	if err != nil {
		logger.Errorf("[CloseOrder] Transaction failed: %v", err.Error())
		return err
	}

	// Update user cache after transaction commits successfully
	if orderInfo.GiftAmount > 0 && orderInfo.UserId != 0 {
		if userInfo, findErr := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId); findErr == nil {
			if clearErr := l.svcCtx.UserModel.ClearUserCache(l.ctx, userInfo); clearErr != nil {
				l.Errorw("[CloseOrder] failed to clear user cache",
					logger.Field("error", clearErr.Error()),
					logger.Field("user_id", orderInfo.UserId),
				)
			}
		}
	}

	return nil
}

// recoverPaidOrder is the "gateway said paid" branch: flip the order to
// status=2 (paid), persist the trade_no if we have one, and enqueue the same
// activation task the notify handlers would have enqueued. We deliberately
// reuse ForthwithActivateOrder so the rest of the activation pipeline
// (idempotency, claim/release, commission, etc.) stays untouched.
func (l *CloseOrderLogic) recoverPaidOrder(orderInfo *order.Order) error {
	updates := map[string]any{"status": 2}
	if orderInfo.TradeNo != "" {
		updates["trade_no"] = orderInfo.TradeNo
	}

	// Race-safe: only flip 1→2. If another worker already moved the order
	// forward (e.g. a late notify finally landed), do nothing.
	result := l.svcCtx.DB.WithContext(l.ctx).
		Model(&order.Order{}).
		Where("order_no = ? AND status = ?", orderInfo.OrderNo, 1).
		Updates(updates)
	if result.Error != nil {
		l.Errorw("[CloseOrder] gateway-paid recovery update failed — keeping order open",
			logger.Field("metric", "close_gateway_error_kept_open"),
			logger.Field("error", result.Error.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return nil
	}
	if result.RowsAffected == 0 {
		l.Infow("[CloseOrder] gateway-paid recovery skipped — order already moved past status=1",
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return nil
	}

	payload := queueTypes.ForthwithActivateOrderPayload{OrderNo: orderInfo.OrderNo}
	bytes, err := json.Marshal(&payload)
	if err != nil {
		l.Errorw("[CloseOrder] marshal activation payload failed",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return nil
	}
	taskID, err := l.enqueueActivate(l.ctx, bytes)
	if err != nil {
		// Order is already at status=2; if enqueue fails the stuck-order
		// recovery sweeper will pick it up. Log loudly but don't error out.
		l.Errorw("[CloseOrder] enqueue activation task failed after gateway-paid recovery",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return nil
	}

	l.Infow("[CloseOrder] gateway-paid recovery succeeded — order promoted to paid + activation enqueued (metric=close_with_gateway_paid_recovered)",
		logger.Field("metric", "close_with_gateway_paid_recovered"),
		logger.Field("orderNo", orderInfo.OrderNo),
		logger.Field("method", orderInfo.Method),
		logger.Field("trade_no", orderInfo.TradeNo),
		logger.Field("queue_task_id", taskID),
	)
	return nil
}

// queryGatewayPaymentStatus dispatches to the right gateway client for the
// order's payment method and returns a tri-state result. Methods without an
// external gateway (Balance, unknown) return Unpaid — the historical close
// path is preserved for them.
func (l *CloseOrderLogic) queryGatewayPaymentStatus(orderInfo *order.Order) GatewayPaymentStatus {
	switch orderInfo.Method {
	case AlipayF2f:
		return l.queryAlipay(orderInfo)
	case StripeAlipay, StripeWeChatPay:
		return l.queryStripe(orderInfo)
	case Epay:
		return l.queryEpay(orderInfo)
	case Balance:
		// Balance is settled in-process; no external gateway to ask. If status=1
		// here, the balance deduction simply never completed — safe to close.
		return GatewayStatusUnpaid
	default:
		l.Infow("[CloseOrder] no gateway query for method — falling back to close",
			logger.Field("method", orderInfo.Method),
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return GatewayStatusUnpaid
	}
}

// queryAlipay queries Alipay F2F and maps the response to a tri-state.
// "trade not exist" on Alipay's side is treated as Unpaid (the user never
// completed the QR-code scan), not Unknown.
func (l *CloseOrderLogic) queryAlipay(orderInfo *order.Order) GatewayPaymentStatus {
	if orderInfo.TradeNo == "" {
		// Alipay F2F creates the trade lazily — no trade_no means the user
		// never scanned. Treat as definitively unpaid.
		return GatewayStatusUnpaid
	}
	paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId)
	if err != nil {
		l.Errorw("[CloseOrder] Find payment config failed",
			logger.Field("error", err.Error()),
			logger.Field("paymentMark", orderInfo.Method),
		)
		return GatewayStatusUnknown
	}
	config := payment.AlipayF2FConfig{}
	if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
		l.Errorw("[CloseOrder] Unmarshal Alipay config failed",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return GatewayStatusUnknown
	}
	client := alipay.NewClient(alipay.Config{
		AppId:       config.AppId,
		PrivateKey:  config.PrivateKey,
		PublicKey:   config.PublicKey,
		InvoiceName: config.InvoiceName,
	})
	if client == nil {
		return GatewayStatusUnknown
	}
	status, err := client.QueryTrade(l.ctx, orderInfo.TradeNo)
	if err != nil {
		l.Errorw("[CloseOrder] Alipay QueryTrade failed",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
			logger.Field("tradeNo", orderInfo.TradeNo),
		)
		return GatewayStatusUnknown
	}
	switch status {
	case alipay.Success, alipay.Finished:
		return GatewayStatusPaid
	case alipay.Pending, alipay.Closed:
		return GatewayStatusUnpaid
	default:
		// Unknown alipay status — be conservative.
		return GatewayStatusUnknown
	}
}

// queryStripe queries Stripe PaymentIntent and maps to a tri-state.
// Any Stripe-side error (network, 5xx, decode) is Unknown — Stripe is the
// gateway most prone to silent webhook drops in this codebase, so we must not
// downgrade query failures to "unpaid".
func (l *CloseOrderLogic) queryStripe(orderInfo *order.Order) GatewayPaymentStatus {
	if orderInfo.TradeNo == "" {
		// Stripe's PaymentIntent ID is written into trade_no at create time.
		// Missing it means the intent was never persisted — treat as unpaid.
		return GatewayStatusUnpaid
	}
	paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId)
	if err != nil {
		l.Errorw("[CloseOrder] Find payment config failed",
			logger.Field("error", err.Error()),
			logger.Field("paymentMark", orderInfo.Method),
		)
		return GatewayStatusUnknown
	}
	config := payment.StripeConfig{}
	if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
		l.Errorw("[CloseOrder] Unmarshal Stripe config failed",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return GatewayStatusUnknown
	}
	client := stripe.NewClient(stripe.Config{
		PublicKey:     config.PublicKey,
		SecretKey:     config.SecretKey,
		WebhookSecret: config.WebhookSecret,
	})
	paid, err := client.QueryOrderStatus(orderInfo.TradeNo)
	if err != nil {
		l.Errorw("[CloseOrder] Stripe QueryOrderStatus failed",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
			logger.Field("tradeNo", orderInfo.TradeNo),
		)
		return GatewayStatusUnknown
	}
	if paid {
		return GatewayStatusPaid
	}
	return GatewayStatusUnpaid
}

// queryEpay queries EPay using out_trade_no (EPay's order endpoint accepts
// out_trade_no even when we never recorded its internal trade_no, which is the
// common case for orders that lost their notify callback).
func (l *CloseOrderLogic) queryEpay(orderInfo *order.Order) GatewayPaymentStatus {
	paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId)
	if err != nil {
		l.Errorw("[CloseOrder] Find payment config failed",
			logger.Field("error", err.Error()),
			logger.Field("paymentMark", orderInfo.Method),
		)
		return GatewayStatusUnknown
	}
	config := payment.EPayConfig{}
	if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
		l.Errorw("[CloseOrder] Unmarshal EPay config failed",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return GatewayStatusUnknown
	}
	if config.Url == "" || config.Pid == "" || config.Key == "" {
		l.Errorw("[CloseOrder] EPay config incomplete",
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return GatewayStatusUnknown
	}
	client := epay.NewClient(config.Pid, config.Url, config.Key, config.Type)
	paid, err := client.QueryOrderStatus(orderInfo.OrderNo)
	if err != nil {
		l.Errorw("[CloseOrder] EPay QueryOrderStatus failed",
			logger.Field("error", err.Error()),
			logger.Field("orderNo", orderInfo.OrderNo),
		)
		return GatewayStatusUnknown
	}
	if paid {
		return GatewayStatusPaid
	}
	return GatewayStatusUnpaid
}
